diff --git a/Channel-Backend/package-lock.json b/Channel-Backend/package-lock.json index b447c1a..9cc6767 100644 --- a/Channel-Backend/package-lock.json +++ b/Channel-Backend/package-lock.json @@ -12,6 +12,7 @@ "@aws-sdk/s3-request-presigner": "^3.1083.0", "@prisma/adapter-pg": "^7.8.0", "@prisma/client": "^7.8.0", + "axios": "^1.18.1", "bcrypt": "^6.0.0", "cheerio": "^1.2.0", "cookie-parser": "^1.4.7", @@ -20,9 +21,12 @@ "express": "^4.19.2", "helmet": "^7.1.0", "jsonwebtoken": "^9.0.3", + "mammoth": "^1.12.0", "multer": "^2.2.0", "nodemailer": "^9.0.3", + "pdf-parse": "^2.4.5", "pg": "^8.22.0", + "xlsx": "^0.18.5", "zod": "^4.4.3" }, "devDependencies": { @@ -34,6 +38,7 @@ "@types/multer": "^2.2.0", "@types/node": "^20.12.7", "@types/nodemailer": "^8.0.1", + "@types/pdf-parse": "^1.1.5", "@types/pg": "^8.20.0", "nodemon": "^3.1.0", "prisma": "^7.8.0", @@ -457,6 +462,190 @@ "devOptional": true, "license": "MIT" }, + "node_modules/@napi-rs/canvas": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.80.tgz", + "integrity": "sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==", + "license": "MIT", + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "0.1.80", + "@napi-rs/canvas-darwin-arm64": "0.1.80", + "@napi-rs/canvas-darwin-x64": "0.1.80", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.80", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.80", + "@napi-rs/canvas-linux-arm64-musl": "0.1.80", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.80", + "@napi-rs/canvas-linux-x64-gnu": "0.1.80", + "@napi-rs/canvas-linux-x64-musl": "0.1.80", + "@napi-rs/canvas-win32-x64-msvc": "0.1.80" + } + }, + "node_modules/@napi-rs/canvas-android-arm64": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.80.tgz", + "integrity": "sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.80.tgz", + "integrity": "sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-darwin-x64": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.80.tgz", + "integrity": "sha512-FqqSU7qFce0Cp3pwnTjVkKjjOtxMqRe6lmINxpIZYaZNnVI0H5FtsaraZJ36SiTHNjZlUB69/HhxNDT1Aaa9vA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.80.tgz", + "integrity": "sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.80.tgz", + "integrity": "sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.80.tgz", + "integrity": "sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.80.tgz", + "integrity": "sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.80.tgz", + "integrity": "sha512-BeXAmhKg1kX3UCrJsYbdQd3hIMDH/K6HnP/pG2LuITaXhXBiNdh//TVVVVCBbJzVQaV5gK/4ZOCMrQW9mvuTqA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.80.tgz", + "integrity": "sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "0.1.80", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.80.tgz", + "integrity": "sha512-Z8jPsM6df5V8B1HrCHB05+bDiCxjE9QA//3YrkKIdVDEwn5RKaqOxCJDRJkl48cJbylcrJbW4HxZbTte8juuPg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, "node_modules/@prisma/adapter-pg": { "version": "7.8.0", "resolved": "https://registry.npmjs.org/@prisma/adapter-pg/-/adapter-pg-7.8.0.tgz", @@ -1068,6 +1257,16 @@ "@types/node": "*" } }, + "node_modules/@types/pdf-parse": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@types/pdf-parse/-/pdf-parse-1.1.5.tgz", + "integrity": "sha512-kBfrSXsloMnUJOKi25s3+hRmkycHfLK6A09eRGqF/N8BkQoPUmaCr+q8Cli5FnfohEz/rsv82zAiPz/LXtOGhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/pg": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz", @@ -1137,6 +1336,15 @@ "@types/node": "*" } }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.13", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.13.tgz", + "integrity": "sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -1176,6 +1384,50 @@ "node": ">=0.4.0" } }, + "node_modules/adler-32": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz", + "integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/ajv": { "version": "8.20.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", @@ -1220,12 +1472,27 @@ "dev": true, "license": "MIT" }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "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", @@ -1236,6 +1503,18 @@ "node": ">= 6.0.0" } }, + "node_modules/axios": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.1.tgz", + "integrity": "sha512-3nTvFlvpn9Zu/RkHUqtc7/+al4UpRW5az71ap5zccp6e8RAYEzhMTecX8Dz1wWDYrPpUoB1HAQEGEAEvUr7S9g==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.16.0", + "form-data": "^4.0.5", + "https-proxy-agent": "^5.0.1", + "proxy-from-env": "^2.1.0" + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", @@ -1246,6 +1525,26 @@ "node": "18 || 20 || >=22" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/bcrypt": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz", @@ -1280,6 +1579,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bluebird": { + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.7.tgz", + "integrity": "sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "1.20.5", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", @@ -1475,6 +1780,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/cfb": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz", + "integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "crc-32": "~1.2.0" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/chart.js": { "version": "4.5.1", "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.5.1.tgz", @@ -1555,6 +1873,27 @@ "fsevents": "~2.3.2" } }, + "node_modules/codepage": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz", + "integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "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/concat-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", @@ -1632,6 +1971,12 @@ "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", "license": "MIT" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.6", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", @@ -1649,6 +1994,18 @@ "url": "https://opencollective.com/express" } }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -1733,6 +2090,15 @@ "devOptional": true, "license": "MIT" }, + "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", @@ -1779,6 +2145,12 @@ "node": ">=0.3.1" } }, + "node_modules/dingbat-to-unicode": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dingbat-to-unicode/-/dingbat-to-unicode-1.0.1.tgz", + "integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==", + "license": "BSD-2-Clause" + }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -1846,6 +2218,15 @@ "url": "https://dotenvx.com" } }, + "node_modules/duck": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/duck/-/duck-0.1.12.tgz", + "integrity": "sha512-wkctla1O6VfP89gQ+J/yDesM0S7B7XLXjKGzXxMDVFg7uEn706niAtyYovKbyq1oT9YwDcly721/iUWoc8MVRg==", + "license": "BSD", + "dependencies": { + "underscore": "^1.13.1" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1985,6 +2366,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", @@ -2131,6 +2527,26 @@ "node": ">= 0.8" } }, + "node_modules/follow-redirects": { + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -2148,6 +2564,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.4", + "mime-types": "^2.1.35" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -2157,6 +2589,15 @@ "node": ">= 0.6" } }, + "node_modules/frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -2322,6 +2763,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.4", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", @@ -2411,6 +2867,42 @@ "devOptional": true, "license": "MIT" }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -2430,6 +2922,12 @@ "dev": true, "license": "ISC" }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "license": "MIT" + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -2498,6 +2996,12 @@ "devOptional": true, "license": "MIT" }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2550,6 +3054,48 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/jszip": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz", + "integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==", + "license": "(MIT OR GPL-3.0-or-later)", + "dependencies": { + "lie": "~3.3.0", + "pako": "~1.0.2", + "readable-stream": "~2.3.6", + "setimmediate": "^1.0.5" + } + }, + "node_modules/jszip/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/jszip/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/jszip/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/jwa": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", @@ -2571,6 +3117,15 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/lie": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", + "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -2620,6 +3175,17 @@ "devOptional": true, "license": "Apache-2.0" }, + "node_modules/lop": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/lop/-/lop-0.4.2.tgz", + "integrity": "sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==", + "license": "BSD-2-Clause", + "dependencies": { + "duck": "^0.1.12", + "option": "~0.2.1", + "underscore": "^1.13.1" + } + }, "node_modules/lru.min": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", @@ -2643,6 +3209,30 @@ "dev": true, "license": "ISC" }, + "node_modules/mammoth": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/mammoth/-/mammoth-1.12.0.tgz", + "integrity": "sha512-cwnK1RIcRdDMi2HRx2EXGYlxqIEh0Oo3bLhorgnsVJi2UkbX1+jKxuBNR9PC5+JaX7EkmJxFPmo6mjLpqShI2w==", + "license": "BSD-2-Clause", + "dependencies": { + "@xmldom/xmldom": "^0.8.6", + "argparse": "~1.0.3", + "base64-js": "^1.5.1", + "bluebird": "~3.4.0", + "dingbat-to-unicode": "^1.0.1", + "jszip": "^3.7.1", + "lop": "^0.4.2", + "path-is-absolute": "^1.0.0", + "underscore": "^1.13.1", + "xmlbuilder": "^10.0.0" + }, + "bin": { + "mammoth": "bin/mammoth" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -2958,6 +3548,18 @@ "node": ">= 0.8" } }, + "node_modules/option": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/option/-/option-0.2.4.tgz", + "integrity": "sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==", + "license": "BSD-2-Clause" + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "license": "(MIT AND Zlib)" + }, "node_modules/parse5": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", @@ -3016,6 +3618,15 @@ "node": ">= 0.8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -3039,6 +3650,38 @@ "devOptional": true, "license": "MIT" }, + "node_modules/pdf-parse": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/pdf-parse/-/pdf-parse-2.4.5.tgz", + "integrity": "sha512-mHU89HGh7v+4u2ubfnevJ03lmPgQ5WU4CxAVmTSh/sxVTEDYd1er/dKS/A6vg77NX47KTEoihq8jZBLr8Cxuwg==", + "license": "Apache-2.0", + "dependencies": { + "@napi-rs/canvas": "0.1.80", + "pdfjs-dist": "5.4.296" + }, + "bin": { + "pdf-parse": "bin/cli.mjs" + }, + "engines": { + "node": ">=20.16.0 <21 || >=22.3.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/mehmet-kozan" + } + }, + "node_modules/pdfjs-dist": { + "version": "5.4.296", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.4.296.tgz", + "integrity": "sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=20.16.0 || >=22.3.0" + }, + "optionalDependencies": { + "@napi-rs/canvas": "^0.1.80" + } + }, "node_modules/perfect-debounce": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", @@ -3256,6 +3899,12 @@ } } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/proper-lockfile": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", @@ -3288,6 +3937,15 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -3542,6 +4200,12 @@ "node": ">= 0.8.0" } }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -3678,6 +4342,12 @@ "node": ">= 10.x" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, "node_modules/sqlstring": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", @@ -3688,6 +4358,18 @@ "node": ">= 0.6" } }, + "node_modules/ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "license": "Apache-2.0", + "dependencies": { + "frac": "~1.1.2" + }, + "engines": { + "node": ">=0.8" + } + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", @@ -3856,6 +4538,12 @@ "dev": true, "license": "MIT" }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "license": "MIT" + }, "node_modules/undici": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", @@ -3976,6 +4664,54 @@ "node": ">= 8" } }, + "node_modules/wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/xlsx": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz", + "integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==", + "license": "Apache-2.0", + "dependencies": { + "adler-32": "~1.3.0", + "cfb": "~1.2.1", + "codepage": "~1.15.0", + "crc-32": "~1.2.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "bin": { + "xlsx": "bin/xlsx.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/xmlbuilder": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", + "integrity": "sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/Channel-Backend/package.json b/Channel-Backend/package.json index 7c6151c..cdd82ba 100644 --- a/Channel-Backend/package.json +++ b/Channel-Backend/package.json @@ -13,6 +13,7 @@ "@aws-sdk/s3-request-presigner": "^3.1083.0", "@prisma/adapter-pg": "^7.8.0", "@prisma/client": "^7.8.0", + "axios": "^1.18.1", "bcrypt": "^6.0.0", "cheerio": "^1.2.0", "cookie-parser": "^1.4.7", @@ -21,9 +22,12 @@ "express": "^4.19.2", "helmet": "^7.1.0", "jsonwebtoken": "^9.0.3", + "mammoth": "^1.12.0", "multer": "^2.2.0", "nodemailer": "^9.0.3", + "pdf-parse": "^2.4.5", "pg": "^8.22.0", + "xlsx": "^0.18.5", "zod": "^4.4.3" }, "devDependencies": { @@ -35,6 +39,7 @@ "@types/multer": "^2.2.0", "@types/node": "^20.12.7", "@types/nodemailer": "^8.0.1", + "@types/pdf-parse": "^1.1.5", "@types/pg": "^8.20.0", "nodemon": "^3.1.0", "prisma": "^7.8.0", diff --git a/Channel-Backend/prisma/schema.prisma b/Channel-Backend/prisma/schema.prisma index b70a701..a14e92c 100644 --- a/Channel-Backend/prisma/schema.prisma +++ b/Channel-Backend/prisma/schema.prisma @@ -24,6 +24,19 @@ model Organization { updatedAt DateTime @updatedAt users User[] sharedAssets SharedAsset[] + branches Branch[] +} + +model Branch { + id String @id @default(uuid()) + name String + code String? @unique + organizationId String + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + users User[] + sharedAssets SharedAsset[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } model User { @@ -36,6 +49,7 @@ model User { inviteToken String? @unique inviteTokenExp DateTime? organizationId String? + branchId String? onboardingStatus String @default("PENDING_ONBOARDING") partnerGroup String? assignedNdaId String? @@ -49,45 +63,50 @@ model User { assignedNda LegalDocument? @relation("AssignedNda", fields: [assignedNdaId], references: [id], onDelete: SetNull) assignedMsa LegalDocument? @relation("AssignedMsa", fields: [assignedMsaId], references: [id], onDelete: SetNull) organization Organization? @relation(fields: [organizationId], references: [id]) + branch Branch? @relation(fields: [branchId], references: [id], onDelete: SetNull) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt acceptances LegalAcceptance[] auditLogs AuditLog[] sharedAssets SharedAsset[] downloadRequests DownloadRequest[] + chatSessions ChatSession[] } model Asset { - id String @id @default(uuid()) - title String - type String - size Int - url String - version Int @default(1) - uploadedBy String - description String? @db.Text - categoryId String? - subcategory String? - tags String[] - downloadsCount Int @default(0) - githubUrl String? - status String @default("published") - isDownloadable Boolean @default(true) - thumbnailUrl String? - problemStatement String? @db.Text - solution String? @db.Text - contentType String? - folderId String? - folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull) - verticals Vertical[] @relation("AssetVerticals") - techStacks TechStack[] @relation("AssetTechStacks") - engagementTypes EngagementType[] @relation("AssetEngagementTypes") - complianceStandards ComplianceStandard[] @relation("AssetComplianceStandards") - sharedWith SharedAsset[] - downloadRequests DownloadRequest[] - assetGroups AssetGroup[] - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(uuid()) + title String + type String + size Int + url String + version Int @default(1) + uploadedBy String + description String? @db.Text + categoryId String? + subcategory String? + tags String[] + downloadsCount Int @default(0) + githubUrl String? + status String @default("published") + isDownloadable Boolean @default(true) + thumbnailUrl String? + problemStatement String? @db.Text + solution String? @db.Text + contentType String? + includeInKnowledgeBase Boolean @default(true) + knowledgeScope String? @default("CATALOG_SHARED") + folderId String? + folder Folder? @relation(fields: [folderId], references: [id], onDelete: SetNull) + verticals Vertical[] @relation("AssetVerticals") + techStacks TechStack[] @relation("AssetTechStacks") + engagementTypes EngagementType[] @relation("AssetEngagementTypes") + complianceStandards ComplianceStandard[] @relation("AssetComplianceStandards") + sharedWith SharedAsset[] + downloadRequests DownloadRequest[] + assetGroups AssetGroup[] + embeddings AssetEmbedding[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } model Vertical { @@ -162,10 +181,12 @@ model SharedAsset { assetId String organizationId String userId String? + branchId String? createdAt DateTime @default(now()) asset Asset @relation(fields: [assetId], references: [id], onDelete: Cascade) organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) user User? @relation(fields: [userId], references: [id], onDelete: Cascade) + branch Branch? @relation(fields: [branchId], references: [id], onDelete: Cascade) @@unique([assetId, organizationId, userId]) } @@ -238,34 +259,69 @@ model AssetGroup { } model EcosystemOffering { - id String @id @default(uuid()) - name String @unique - type String // "PRODUCT" | "SERVICE" - tagline String - description String @db.Text - benefits String[] - websiteUrl String - ctaText String @default("Visit Website") - logoIcon String @default("Globe") - logoUrl String? - mediaUrl String? - mediaType String? // "IMAGE" | "VIDEO" | "GIF" - orderIndex Int @default(0) - isActive Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(uuid()) + name String @unique + type String // "PRODUCT" | "SERVICE" + tagline String + description String @db.Text + benefits String[] + websiteUrl String + ctaText String @default("Visit Website") + logoIcon String @default("Globe") + logoUrl String? + mediaUrl String? + mediaType String? // "IMAGE" | "VIDEO" | "GIF" + includeInKnowledgeBase Boolean @default(true) + orderIndex Int @default(0) + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt } model ContentShowcase { - id String @id @default(uuid()) - title String - description String? @db.Text - youtubeUrl String - thumbnailUrl String? - redirectUrl String? - redirectLabel String? @default("Learn More") - orderIndex Int @default(0) - isActive Boolean @default(true) - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt + id String @id @default(uuid()) + title String + description String? @db.Text + youtubeUrl String + thumbnailUrl String? + redirectUrl String? + redirectLabel String? @default("Learn More") + includeInKnowledgeBase Boolean @default(true) + orderIndex Int @default(0) + isActive Boolean @default(true) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model AssetEmbedding { + id String @id @default(uuid()) + assetId String + chunkIndex Int + chunkType String @default("TEXT") // TEXT, SLIDE, SHEET, PAGE, TRANSCRIPT + sourceMetadata Json? // OKF Standard JSON: { title, location: "Page 4" | "Slide 2" | "Sheet Data", okfCategory } + content String @db.Text + vector String // Vector array string representation for similarity search + createdAt DateTime @default(now()) + asset Asset @relation(fields: [assetId], references: [id], onDelete: Cascade) + + @@index([assetId]) +} + +model ChatSession { + id String @id @default(uuid()) + userId String + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + messages ChatMessage[] + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model ChatMessage { + id String @id @default(uuid()) + sessionId String + session ChatSession @relation(fields: [sessionId], references: [id], onDelete: Cascade) + sender String // "USER" | "ASSISTANT" + content String @db.Text + citations Json? // Array of OKF standardized source citations + createdAt DateTime @default(now()) } diff --git a/Channel-Backend/src/app.ts b/Channel-Backend/src/app.ts index 973a86f..8429e5d 100644 --- a/Channel-Backend/src/app.ts +++ b/Channel-Backend/src/app.ts @@ -18,6 +18,8 @@ import legalRoutes from './routes/legal.routes'; import ecosystemRoutes from './routes/ecosystem.routes'; import taxonomyRoutes from './routes/taxonomy.routes'; import notificationRoutes from './routes/notification.routes'; +import chatRoutes from './routes/chat.routes'; +import branchRoutes from './routes/branch.routes'; import { ensureBucketExists } from './utils/s3'; import { originStorage } from './utils/origin-storage'; @@ -113,6 +115,8 @@ app.use('/api/v1/organizations', orgRoutes); app.use('/api/v1/legal', legalRoutes); app.use('/api/v1/ecosystem', ecosystemRoutes); app.use('/api/v1/taxonomy', taxonomyRoutes); +app.use('/api/v1/chat', chatRoutes); +app.use('/api/v1', branchRoutes); app.get('/api/v1/health', (req: Request, res: Response) => { res.status(200).json({ status: 'success', message: 'API is fully functional and real.' }); diff --git a/Channel-Backend/src/controllers/branch.controller.ts b/Channel-Backend/src/controllers/branch.controller.ts new file mode 100644 index 0000000..c96cbf9 --- /dev/null +++ b/Channel-Backend/src/controllers/branch.controller.ts @@ -0,0 +1,39 @@ +import { Response, NextFunction } from 'express'; +import { AuthRequest } from '../middleware/auth.middleware'; +import { BranchService } from '../services/branch.service'; + +const branchService = new BranchService(); + +export class BranchController { + public createBranch = async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const { organizationId } = req.params; + const branch = await branchService.createBranch(organizationId, req.body); + res.status(201).json(branch); + } catch (err) { next(err); } + }; + + public getBranches = async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const { organizationId } = req.params; + const branches = await branchService.getOrganizationBranches(organizationId); + res.status(200).json(branches); + } catch (err) { next(err); } + }; + + public updateBranch = async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const { id } = req.params; + const branch = await branchService.updateBranch(id, req.body); + res.status(200).json(branch); + } catch (err) { next(err); } + }; + + public deleteBranch = async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const { id } = req.params; + await branchService.deleteBranch(id); + res.status(200).json({ message: 'Branch deleted successfully' }); + } catch (err) { next(err); } + }; +} diff --git a/Channel-Backend/src/controllers/chat.controller.ts b/Channel-Backend/src/controllers/chat.controller.ts new file mode 100644 index 0000000..4c262d0 --- /dev/null +++ b/Channel-Backend/src/controllers/chat.controller.ts @@ -0,0 +1,107 @@ +import { Response, NextFunction } from 'express'; +import { AuthRequest } from '../middleware/auth.middleware'; +import { ChatService } from '../services/chat.service'; +import prisma from '../utils/db'; + +const chatService = new ChatService(); + +export class ChatController { + public queryChat = async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const { prompt, sessionId } = req.body; + if (!prompt || !prompt.trim()) { + return res.status(400).json({ error: 'Prompt is required' }); + } + + const userId = req.user?.userId; + if (!userId) { + return res.status(401).json({ error: 'Unauthorized' }); + } + + const result = await chatService.processPrompt(userId, prompt, sessionId); + res.status(200).json(result); + } catch (err) { + next(err); + } + }; + + public getHistory = async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const userId = req.user?.userId; + if (!userId) { + return res.status(401).json({ error: 'Unauthorized' }); + } + + const sessions = await prisma.chatSession.findMany({ + where: { userId }, + include: { + messages: { + take: 2, + orderBy: { createdAt: 'asc' } + } + }, + orderBy: { updatedAt: 'desc' }, + take: 20, + }); + + res.status(200).json(sessions); + } catch (err) { + next(err); + } + }; + + public createSession = async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const userId = req.user?.userId; + if (!userId) { + return res.status(401).json({ error: 'Unauthorized' }); + } + + const session = await prisma.chatSession.create({ + data: { userId } + }); + + res.status(201).json({ sessionId: session.id, session }); + } catch (err) { + next(err); + } + }; + + public getSessionMessages = async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const userId = req.user?.userId; + const { sessionId } = req.params; + + if (!userId) { + return res.status(401).json({ error: 'Unauthorized' }); + } + + const session = await prisma.chatSession.findFirst({ + where: { id: sessionId, userId }, + include: { + messages: { + orderBy: { createdAt: 'asc' } + } + } + }); + + if (!session) { + return res.status(404).json({ error: 'Chat session not found' }); + } + + res.status(200).json(session); + } catch (err) { + next(err); + } + }; + + public syncAssetKnowledge = async (req: AuthRequest, res: Response, next: NextFunction) => { + try { + const { assetId } = req.params; + await chatService.ingestAssetKnowledge(assetId); + res.status(200).json({ message: 'Asset knowledge re-indexed successfully' }); + } catch (err) { + next(err); + } + }; +} diff --git a/Channel-Backend/src/routes/branch.routes.ts b/Channel-Backend/src/routes/branch.routes.ts new file mode 100644 index 0000000..8530b78 --- /dev/null +++ b/Channel-Backend/src/routes/branch.routes.ts @@ -0,0 +1,15 @@ +import { Router } from 'express'; +import { BranchController } from '../controllers/branch.controller'; +import { authenticate, requireRole } from '../middleware/auth.middleware'; + +const router = Router(); +const branchController = new BranchController(); + +router.use(authenticate); + +router.get('/organizations/:organizationId/branches', branchController.getBranches); +router.post('/organizations/:organizationId/branches', requireRole('ADMIN'), branchController.createBranch); +router.put('/branches/:id', requireRole('ADMIN'), branchController.updateBranch); +router.delete('/branches/:id', requireRole('ADMIN'), branchController.deleteBranch); + +export default router; diff --git a/Channel-Backend/src/routes/chat.routes.ts b/Channel-Backend/src/routes/chat.routes.ts new file mode 100644 index 0000000..7387b4b --- /dev/null +++ b/Channel-Backend/src/routes/chat.routes.ts @@ -0,0 +1,16 @@ +import { Router } from 'express'; +import { ChatController } from '../controllers/chat.controller'; +import { authenticate, requireRole } from '../middleware/auth.middleware'; + +const router = Router(); +const chatController = new ChatController(); + +router.use(authenticate); + +router.post('/query', chatController.queryChat); +router.get('/history', chatController.getHistory); +router.post('/sessions', chatController.createSession); +router.get('/sessions/:sessionId', chatController.getSessionMessages); +router.post('/sync/:assetId', requireRole('ADMIN'), chatController.syncAssetKnowledge); + +export default router; diff --git a/Channel-Backend/src/services/branch.service.ts b/Channel-Backend/src/services/branch.service.ts new file mode 100644 index 0000000..0007266 --- /dev/null +++ b/Channel-Backend/src/services/branch.service.ts @@ -0,0 +1,38 @@ +import prisma from '../utils/db'; + +export class BranchService { + public async createBranch(organizationId: string, data: { name: string; code?: string }) { + return prisma.branch.create({ + data: { + name: data.name, + code: data.code || null, + organizationId, + } + }); + } + + public async getOrganizationBranches(organizationId: string) { + return prisma.branch.findMany({ + where: { organizationId }, + include: { + _count: { + select: { users: true, sharedAssets: true } + } + }, + orderBy: { createdAt: 'desc' } + }); + } + + public async updateBranch(id: string, data: { name?: string; code?: string }) { + return prisma.branch.update({ + where: { id }, + data, + }); + } + + public async deleteBranch(id: string) { + return prisma.branch.delete({ + where: { id } + }); + } +} diff --git a/Channel-Backend/src/services/chat.service.ts b/Channel-Backend/src/services/chat.service.ts new file mode 100644 index 0000000..a1914b2 --- /dev/null +++ b/Channel-Backend/src/services/chat.service.ts @@ -0,0 +1,415 @@ +import prisma from '../utils/db'; +import axios from 'axios'; +import { ExtractionService, OKFMetadata } from './extraction.service'; + +export interface CitationItem { + assetId: string; + title: string; + location: string; + type: string; + isRecommended?: boolean; +} + +export class ChatService { + private extractionService = new ExtractionService(); + private gatewayUrl = process.env.LLM_GATEWAY_URL || 'https://llm-devs.tech4biz.org/v1'; + private apiKey = process.env.LLM_API_KEY || 'sk-TM-gxAudDVRTY-kq7_enEA'; + private modelId = process.env.LLM_MODEL_ID || 'deepseek-chat'; + + /** + * Process a user chat prompt with RBAC-scoped, unified RAG retrieval across Assets, Showcase Reels, Ecosystem Offerings, and Legal Documents. + */ + public async processPrompt(userId: string, prompt: string, sessionId?: string) { + const user = await prisma.user.findUnique({ + where: { id: userId }, + include: { + sharedAssets: true, + assignedNda: true, + assignedMsa: true, + } + }); + + if (!user) { + throw new Error('User session not found'); + } + + const partnerGroup = user.partnerGroup ? user.partnerGroup.trim().toLowerCase() : ''; + const isClient = user.role === 'PARTNER_USER'; + + // 1. Determine accessible catalog asset IDs + let accessibleAssetIds: string[] = []; + + if (user.role === 'ADMIN') { + const allAssets = await prisma.asset.findMany({ select: { id: true } }); + accessibleAssetIds = allAssets.map(a => a.id); + } else { + const shared = await prisma.sharedAsset.findMany({ + where: { + OR: [ + { organizationId: user.organizationId || '' }, + { userId: user.id }, + user.branchId ? { branchId: user.branchId } : {}, + ] + }, + select: { assetId: true } + }); + const sharedIds = shared.map(s => s.assetId); + + let groupIds: string[] = []; + if (partnerGroup) { + const groups = await prisma.assetGroup.findMany({ + include: { assets: { select: { id: true } } } + }); + const matchedGroup = groups.find(g => g.name.trim().toLowerCase() === partnerGroup); + if (matchedGroup) { + groupIds = matchedGroup.assets.map(a => a.id); + } + } + + const publicAssets = await prisma.asset.findMany({ + where: { includeInKnowledgeBase: true }, + select: { id: true } + }); + const publicIds = publicAssets.map(a => a.id); + + accessibleAssetIds = Array.from(new Set([...sharedIds, ...groupIds, ...publicIds])); + } + + // 2. Fetch Embeddings & Data Sources + let embeddingsCount = await prisma.assetEmbedding.count({ + where: { assetId: { in: accessibleAssetIds } } + }); + + if (embeddingsCount === 0 && accessibleAssetIds.length > 0) { + await this.autoIndexCatalog(accessibleAssetIds); + } + + const embeddings = await prisma.assetEmbedding.findMany({ + where: { + assetId: { in: accessibleAssetIds }, + asset: { includeInKnowledgeBase: true } + }, + include: { + asset: { + select: { + id: true, + title: true, + type: true, + tags: true, + description: true, + problemStatement: true, + solution: true, + assetGroups: { select: { name: true } } + } + } + } + }); + + // 3. Fetch Featured Content (ContentShowcase) & Ecosystem Offerings + const showcaseItems = await prisma.contentShowcase.findMany({ + where: { isActive: true } + }); + + const ecosystemOfferings = await prisma.ecosystemOffering.findMany({ + where: { isActive: true } + }); + + // 4. Hybrid Search Engine with Normalized Fuzzy Term Matching + const normalizeStr = (str: string) => str.toLowerCase().replace(/[^a-z0-9]/g, ''); + + const promptLower = prompt.toLowerCase().trim(); + const promptNorm = normalizeStr(prompt); + const promptTerms = promptLower.split(/\s+/).filter(w => w.length > 2); + const promptVector = JSON.parse(this.extractionService.generateEmbedding(prompt)); + + const citationsMap = new Map(); + const contextLines: string[] = []; + + // A. Match ContentShowcase items (e.g., "digitaltwin", "digital twin", "aura", "smart basket", "jonas blue") + showcaseItems.forEach((sc) => { + const fullText = (sc.title + ' ' + (sc.description || '')).toLowerCase(); + const textNorm = normalizeStr(fullText); + + let matchCount = 0; + promptTerms.forEach(term => { + const termNorm = normalizeStr(term); + if (termNorm && (fullText.includes(term) || textNorm.includes(termNorm))) { + matchCount++; + } + }); + + const isDirectMatch = (promptNorm.length >= 4 && textNorm.includes(promptNorm)) || + promptLower.includes(sc.title.toLowerCase()) || + sc.title.toLowerCase().includes(promptLower) || + (promptNorm.includes('digitaltwin') && textNorm.includes('digitaltwin')) || + (promptNorm.includes('aura') && textNorm.includes('aura')) || + (promptNorm.includes('smartbasket') && textNorm.includes('smartbasket')) || + (promptNorm.includes('jonasblue') && textNorm.includes('jonasblue')); + + if (isDirectMatch || matchCount >= 1) { + citationsMap.set(sc.id, { + assetId: sc.id, + title: sc.title, + location: 'Featured Content Showcase', + type: 'case_study', + isRecommended: false, + }); + + contextLines.push( + `[Featured Content Reel] Title: "${sc.title}" | ID: "${sc.id}" | URL: "${sc.youtubeUrl}" | Description:\n${sc.description || 'Interactive product reel'}\n` + ); + } + }); + + // B. Match EcosystemOffering items + ecosystemOfferings.forEach((eo) => { + const fullText = (eo.name + ' ' + eo.tagline + ' ' + eo.description).toLowerCase(); + const textNorm = normalizeStr(fullText); + + let matchCount = 0; + promptTerms.forEach(term => { + const termNorm = normalizeStr(term); + if (termNorm && (fullText.includes(term) || textNorm.includes(termNorm))) { + matchCount++; + } + }); + + const isDirectMatch = (promptNorm.length >= 4 && textNorm.includes(promptNorm)) || + promptLower.includes(eo.name.toLowerCase()) || + eo.name.toLowerCase().includes(promptLower); + + if (isDirectMatch || matchCount >= 1) { + citationsMap.set(eo.id, { + assetId: eo.id, + title: eo.name, + location: 'Ecosystem Offering', + type: 'offering', + isRecommended: false, + }); + + contextLines.push( + `[Ecosystem Offering] Name: "${eo.name}" | Type: "${eo.type}" | Tagline: "${eo.tagline}" | Website: "${eo.websiteUrl}" | Description:\n${eo.description}\n` + ); + } + }); + + // C. Hybrid Vector & Keyword Search on Catalog Assets + const isPureNavPrompt = promptLower.includes('theme') || promptLower.includes('dark mode') || promptLower.includes('light mode') || promptLower.includes('how to change') || promptLower.includes('appearance'); + + if (!isPureNavPrompt) { + const scoredChunks = embeddings.map(emb => { + let vectorScore = 0; + try { + const vec = JSON.parse(emb.vector) as number[]; + vectorScore = promptVector.reduce((acc: number, val: number, i: number) => acc + val * (vec[i] || 0), 0); + } catch { + vectorScore = 0; + } + + const chunkText = (emb.content + ' ' + emb.asset.title + ' ' + (emb.asset.description || '')).toLowerCase(); + let keywordMatches = 0; + promptTerms.forEach(term => { + if (chunkText.includes(term)) keywordMatches += 1; + }); + + const hybridScore = vectorScore * 0.5 + (keywordMatches / Math.max(promptTerms.length, 1)) * 0.5; + return { chunk: emb, score: hybridScore, keywordMatches }; + }).sort((a, b) => b.score - a.score); + + const topAssetChunks = scoredChunks.filter(({ score, keywordMatches }) => score >= 0.15 || keywordMatches > 0).slice(0, 3); + + topAssetChunks.forEach(({ chunk }, idx) => { + const meta = (chunk.sourceMetadata as unknown as OKFMetadata) || { + assetId: chunk.assetId, + assetTitle: chunk.asset.title, + assetType: chunk.asset.type, + location: `Segment ${chunk.chunkIndex + 1}`, + }; + + const isRecommended = partnerGroup + ? chunk.asset.assetGroups.some(g => g.name.trim().toLowerCase() === partnerGroup) + : false; + + citationsMap.set(chunk.assetId, { + assetId: chunk.assetId, + title: chunk.asset.title, + location: meta.location, + type: chunk.asset.type, + isRecommended, + }); + + contextLines.push( + `[Catalog Asset ${idx + 1}] Title: "${chunk.asset.title}" | ID: "${chunk.assetId}" | Location: "${meta.location}" | Details:\n${chunk.content}\n` + ); + }); + } + + // D. Inject Assigned Legal Documents (NDA / MSA) if prompt asks about NDA / Legal + if (promptLower.includes('nda') || promptLower.includes('msa') || promptLower.includes('agreement') || promptLower.includes('legal') || promptLower.includes('contract')) { + const legals = await prisma.legalDocument.findMany(); + legals.forEach((l: { type: string; version: string; content: string }, i: number) => { + contextLines.push( + `[Legal Document ${i + 1}] Type: "${l.type}" | Version: "${l.version}" | Content:\n${l.content.slice(0, 600)}...\n` + ); + }); + } + + const contextBlock = contextLines.length > 0 + ? contextLines.join('\n---\n') + : 'No specific catalog or showcase snippets needed for this query.'; + + const portalGuideContext = isClient ? ` +Exact Client Portal Layout & Step-by-Step Navigation Guide (${user.email}): +1. Legal Agreements & NDA/MSA Documents: + - Where to find it: Home Dashboard -> Click the "Legal Agreements" card (or navigate directly to /client/agreements). + - Note: There is NO "Legal Agreements" item in the left sidebar menu. Access it exclusively via the Home Dashboard card "Legal Agreements" or direct URL /client/agreements. + - Assigned NDA: ${user.assignedNda ? `Tech4Biz Standard NDA (v${user.assignedNda.version})` : 'Tech4Biz Standard NDA Agreement'} (Status: Active). + - Assigned MSA: ${user.assignedMsa ? `Tech4Biz Standard MSA (v${user.assignedMsa.version})` : 'Tech4Biz Standard MSA Agreement'} (Status: Active). + +2. Asset Explorer (/client/assets): + - Where to find it: Left Sidebar menu -> Click "Asset Explorer" (or Home Dashboard -> "Asset Library" card). + - Features: Search shared catalog assets, filter by Taxonomy tags (Industry Verticals, Tech Stacks, Engagement Types, Compliance), view pitch decks, and request downloads. + +3. Featured Content Showcase (/client/showcase): + - Where to find it: Left Sidebar menu -> Click "Featured Content". + - Features: Explore interactive video walk-throughs, YouTube/Instagram demo reels, and case studies (including Digital Twin, Diabetic Patient Time Travel, AuRa, Smart Basket, and AI reels). + +4. Explore Ecosystem Offerings (/client/ecosystem): + - Where to find it: Left Sidebar menu -> Click "Explore More" (or Home Dashboard -> "Explore More" card). + - Features: Explore ecosystem products & partner integrations (CodeNuk, Cloudtopiaa). + +5. Dark/Light Theme & Profile Settings: + - Theme Toggle: Click the Sun/Moon icon located in the bottom area of the left Sidebar under "Appearance". + - Profile Settings: Click "Profile Settings" in the left Sidebar menu to update password or default theme preferences. +` : ` +Exact Admin Console Layout Guide for Administrator (${user.email}): +1. Partner Directory (/admin/partners): Left Sidebar -> "Partners". +2. Approvals Queue (/admin/approvals): Left Sidebar -> "Approvals Queue". +3. Legal Templates (/admin/legal): Left Sidebar -> "Legal Templates". +4. Catalog Management (/admin/assets): Left Sidebar -> "Manage Catalog". +5. Ecosystem Manager (/admin/ecosystem): Left Sidebar -> "Ecosystem Manager". +`; + + const systemPrompt = `You are Tech4Biz AI Advisor, the official enterprise assistant for the Channel Partner Portal. +Your task is to provide accurate, professional, and clear answers to user questions. + +Role & Context: +- User: ${user.email} (${isClient ? 'Client / Partner' : 'Portal Administrator'}). +- STIPULATION: NEVER tell a Client user to look for "Legal Agreements" in the left sidebar. State clearly: "Click the 'Legal Agreements' card on your Home Dashboard or go to /client/agreements". +- STIPULATION: NEVER mention admin console routes (/admin, Partner Directory, Legal Templates) when talking to a Client user. + +Knowledge Guidelines: +- If the user asks about a specific Featured Content video or Case Study (such as "digital twin", "digitaltwin", "where is digital twin", "what is digital twin", "smart basket", "aura", "jonas blue"), identify the exact showcase item from Knowledge Context and summarize its title, description, and YouTube link. +- Answer catalog/product questions using the provided Catalog Asset Snippets. +- For navigation or portal help, use the exact Client Portal Layout & Step-by-Step Navigation Guide. +- Use bold headers and clear formatting in Markdown.`; + + const messages = [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: `Platform Navigation Guide:\n${portalGuideContext}\n\nKnowledge Context:\n${contextBlock}\n\nUser Question: ${prompt}` } + ]; + + // 5. Call DeepSeek LLM Gateway API + let assistantReply = ''; + try { + const response = await axios.post(`${this.gatewayUrl}/chat/completions`, { + model: this.modelId, + messages, + temperature: 0.2, + }, { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${this.apiKey}` + } + }); + + assistantReply = response.data.choices[0]?.message?.content || 'No response generated from AI Engine.'; + } catch (err: any) { + console.error('DeepSeek Gateway Error:', err?.response?.data || err.message); + assistantReply = 'Here is the requested information from your shared catalog:\n\n' + contextBlock; + } + + const citations = Array.from(citationsMap.values()); + + // 6. Save Chat Session & Messages + let activeSessionId = sessionId; + if (!activeSessionId) { + const session = await prisma.chatSession.create({ + data: { userId } + }); + activeSessionId = session.id; + } + + await prisma.chatMessage.create({ + data: { + sessionId: activeSessionId, + sender: 'USER', + content: prompt, + } + }); + + const botMessage = await prisma.chatMessage.create({ + data: { + sessionId: activeSessionId, + sender: 'ASSISTANT', + content: assistantReply, + citations: citations as any, + } + }); + + return { + sessionId: activeSessionId, + message: botMessage, + citations, + }; + } + + /** + * Auto-indexes active catalog assets into AssetEmbedding for instant knowledge retrieval. + */ + public async autoIndexCatalog(assetIds: string[]) { + const assets = await prisma.asset.findMany({ + where: { + id: { in: assetIds }, + includeInKnowledgeBase: true, + } + }); + + for (const asset of assets) { + await this.ingestAssetKnowledge(asset.id); + } + } + + /** + * Ingest or re-index an asset's text into vector embeddings. + */ + public async ingestAssetKnowledge(assetId: string) { + const asset = await prisma.asset.findUnique({ + where: { id: assetId } + }); + + if (!asset) return; + + await prisma.assetEmbedding.deleteMany({ + where: { assetId } + }); + + if (!asset.includeInKnowledgeBase) return; + + const okfChunks = await this.extractionService.extractOKFChunks(asset); + + for (const chunk of okfChunks) { + const vector = this.extractionService.generateEmbedding(chunk.content); + await prisma.assetEmbedding.create({ + data: { + assetId, + chunkIndex: chunk.chunkIndex, + chunkType: chunk.chunkType, + sourceMetadata: chunk.sourceMetadata as any, + content: chunk.content, + vector, + } + }); + } + } +} diff --git a/Channel-Backend/src/services/extraction.service.ts b/Channel-Backend/src/services/extraction.service.ts new file mode 100644 index 0000000..e4e78c1 --- /dev/null +++ b/Channel-Backend/src/services/extraction.service.ts @@ -0,0 +1,285 @@ +import fs from 'fs'; +import path from 'path'; +const pdfParse = require('pdf-parse'); +import mammoth from 'mammoth'; +import * as XLSX from 'xlsx'; +import * as cheerio from 'cheerio'; +import { GetObjectCommand } from '@aws-sdk/client-s3'; +import { s3Client, BUCKET_NAME } from '../utils/s3'; +import { ScraperService } from './scraper.service'; + +export interface OKFMetadata { + assetId: string; + assetTitle: string; + assetType: string; + location: string; // e.g. "Page 3", "Slide 5", "Sheet: Financials", "Transcript", "Section: Architecture" + okfCategory?: string; + isRecommended?: boolean; +} + +export interface OKFChunk { + chunkIndex: number; + chunkType: 'PAGE' | 'SLIDE' | 'SHEET' | 'TRANSCRIPT' | 'TEXT'; + content: string; + sourceMetadata: OKFMetadata; +} + +export class ExtractionService { + private scraperService = new ScraperService(); + + /** + * Simple, fast deterministic embedding generator for local RAG vector search. + * Generates a 64-dimensional float vector normalized for cosine similarity. + */ + public generateEmbedding(text: string): string { + const dim = 64; + const vector = new Array(dim).fill(0); + const cleaned = text.toLowerCase().replace(/[^\w\s]/g, ''); + const words = cleaned.split(/\s+/).filter(Boolean); + + for (let i = 0; i < words.length; i++) { + const word = words[i]; + for (let j = 0; j < word.length; j++) { + const charCode = word.charCodeAt(j); + const idx = (charCode + j * 7 + i * 3) % dim; + vector[idx] += 1; + } + } + + // L2 Normalize + const magnitude = Math.sqrt(vector.reduce((sum, val) => sum + val * val, 0)) || 1; + const normalized = vector.map(v => Number((v / magnitude).toFixed(6))); + + return JSON.stringify(normalized); + } + + /** + * Extract text chunks from an asset binary or URL using the Open Knowledge Framework (OKF) standard format. + */ + public async extractOKFChunks(asset: { + id: string; + title: string; + type: string; + url: string; + problemStatement?: string | null; + solution?: string | null; + description?: string | null; + }): Promise { + const chunks: OKFChunk[] = []; + let chunkIndex = 0; + + // 1. Ingest Problem Statement & Solution metadata if present + if (asset.problemStatement) { + chunks.push({ + chunkIndex: chunkIndex++, + chunkType: 'TEXT', + content: `Problem Statement for ${asset.title}: ${asset.problemStatement}`, + sourceMetadata: { + assetId: asset.id, + assetTitle: asset.title, + assetType: asset.type, + location: 'Executive Overview: Problem Statement', + }, + }); + } + + if (asset.solution) { + chunks.push({ + chunkIndex: chunkIndex++, + chunkType: 'TEXT', + content: `Solution Overview for ${asset.title}: ${asset.solution}`, + sourceMetadata: { + assetId: asset.id, + assetTitle: asset.title, + assetType: asset.type, + location: 'Executive Overview: Proposed Solution', + }, + }); + } + + // 2. URL Assets / YouTube / Web scraper handling + if (asset.type === 'url' || asset.type === 'case_study' || asset.url.startsWith('http')) { + try { + const scraped = await this.scraperService.scrapeCaseStudy(asset.url); + const fullContent = `${scraped.title}. ${scraped.problemStatement || ''} ${scraped.solution || ''}`; + + // Split into 500-token chunks + const subChunks = this.splitText(fullContent, 500); + subChunks.forEach((text, i) => { + chunks.push({ + chunkIndex: chunkIndex++, + chunkType: 'TRANSCRIPT', + content: text, + sourceMetadata: { + assetId: asset.id, + assetTitle: asset.title, + assetType: asset.type, + location: `Web Link Content: Part ${i + 1}`, + }, + }); + }); + } catch { + if (asset.description) { + chunks.push({ + chunkIndex: chunkIndex++, + chunkType: 'TEXT', + content: `${asset.title}: ${asset.description}`, + sourceMetadata: { + assetId: asset.id, + assetTitle: asset.title, + assetType: asset.type, + location: 'URL Asset Metadata', + }, + }); + } + } + return chunks; + } + + // 3. Binary S3 Assets (PDF, Word, Excel, PPTX, Text) + if (asset.url.startsWith('/uploads/')) { + const fileKey = asset.url.replace('/uploads/', ''); + let buffer: Buffer; + + try { + const response = await s3Client.send(new GetObjectCommand({ + Bucket: BUCKET_NAME, + Key: fileKey, + })); + const byteArray = await response.Body?.transformToByteArray(); + if (!byteArray) return chunks; + buffer = Buffer.from(byteArray); + } catch (err) { + console.error(`Failed to fetch S3 object ${fileKey} for extraction:`, err); + return chunks; + } + + const ext = path.extname(fileKey).toLowerCase(); + + // A. PDF Files + if (ext === '.pdf' || asset.type.includes('pdf')) { + try { + const pdfData = await pdfParse(buffer); + // Split by pages if possible or chunk content + const subChunks = this.splitText(pdfData.text, 600); + subChunks.forEach((text, i) => { + chunks.push({ + chunkIndex: chunkIndex++, + chunkType: 'PAGE', + content: text, + sourceMetadata: { + assetId: asset.id, + assetTitle: asset.title, + assetType: asset.type, + location: `PDF Document: Page ${i + 1}`, + }, + }); + }); + } catch (e) { console.error('PDF extraction error:', e); } + } + // B. Word Files (.docx, .doc) + else if (ext === '.docx' || ext === '.doc' || asset.type.includes('word')) { + try { + const docResult = await mammoth.extractRawText({ buffer }); + const subChunks = this.splitText(docResult.value, 600); + subChunks.forEach((text, i) => { + chunks.push({ + chunkIndex: chunkIndex++, + chunkType: 'TEXT', + content: text, + sourceMetadata: { + assetId: asset.id, + assetTitle: asset.title, + assetType: asset.type, + location: `Word Document: Section ${i + 1}`, + }, + }); + }); + } catch (e) { console.error('Docx extraction error:', e); } + } + // C. Excel & CSV Files (.xlsx, .xls, .csv) + else if (ext === '.xlsx' || ext === '.xls' || ext === '.csv' || asset.type.includes('spreadsheet') || asset.type.includes('csv')) { + try { + const workbook = XLSX.read(buffer, { type: 'buffer' }); + workbook.SheetNames.forEach((sheetName) => { + const sheet = workbook.Sheets[sheetName]; + const csvText = XLSX.utils.sheet_to_csv(sheet); + if (csvText && csvText.trim()) { + chunks.push({ + chunkIndex: chunkIndex++, + chunkType: 'SHEET', + content: `Sheet [${sheetName}] Data:\n${csvText.slice(0, 1500)}`, + sourceMetadata: { + assetId: asset.id, + assetTitle: asset.title, + assetType: asset.type, + location: `Spreadsheet Sheet: ${sheetName}`, + }, + }); + } + }); + } catch (e) { console.error('Excel extraction error:', e); } + } + // D. PowerPoint Presentations (.pptx, .ppt) + else if (ext === '.pptx' || ext === '.ppt' || asset.type.includes('presentation')) { + // Simple text extraction from raw slide XML/strings + const rawString = buffer.toString('utf-8').replace(/[^\x20-\x7E]/g, ' '); + const subChunks = this.splitText(rawString, 600); + subChunks.forEach((text, i) => { + if (text.length > 50) { + chunks.push({ + chunkIndex: chunkIndex++, + chunkType: 'SLIDE', + content: text, + sourceMetadata: { + assetId: asset.id, + assetTitle: asset.title, + assetType: asset.type, + location: `PowerPoint Presentation: Slide Section ${i + 1}`, + }, + }); + } + }); + } + // E. Plain Text / Markdown + else { + const textContent = buffer.toString('utf-8'); + const subChunks = this.splitText(textContent, 600); + subChunks.forEach((text, i) => { + chunks.push({ + chunkIndex: chunkIndex++, + chunkType: 'TEXT', + content: text, + sourceMetadata: { + assetId: asset.id, + assetTitle: asset.title, + assetType: asset.type, + location: `Document Text: Segment ${i + 1}`, + }, + }); + }); + } + } + + return chunks; + } + + private splitText(text: string, maxLen: number): string[] { + const cleaned = text.replace(/\s+/g, ' ').trim(); + if (!cleaned) return []; + const words = cleaned.split(' '); + const chunks: string[] = []; + let current = ''; + + for (const word of words) { + if ((current + ' ' + word).length > maxLen) { + if (current) chunks.push(current.trim()); + current = word; + } else { + current += (current ? ' ' : '') + word; + } + } + if (current.trim()) chunks.push(current.trim()); + return chunks; + } +} diff --git a/Channel-Frontend/src/app/layouts/AdminLayout.tsx b/Channel-Frontend/src/app/layouts/AdminLayout.tsx index b77e0eb..7cc6e27 100644 --- a/Channel-Frontend/src/app/layouts/AdminLayout.tsx +++ b/Channel-Frontend/src/app/layouts/AdminLayout.tsx @@ -17,6 +17,7 @@ import { Globe } from "lucide-react"; import { motion, AnimatePresence } from "framer-motion"; +import { ChatDrawer } from "../../components/ui/ChatDrawer"; export const AdminLayout: React.FC = () => { const { user, logout } = useAuthStore(); @@ -283,6 +284,7 @@ export const AdminLayout: React.FC = () => { + ); }; diff --git a/Channel-Frontend/src/app/layouts/ClientLayout.tsx b/Channel-Frontend/src/app/layouts/ClientLayout.tsx index c5303a9..f1a3370 100644 --- a/Channel-Frontend/src/app/layouts/ClientLayout.tsx +++ b/Channel-Frontend/src/app/layouts/ClientLayout.tsx @@ -4,6 +4,7 @@ import { useThemeStore } from '../../hooks/use-theme'; import { useAuthStore } from '../../hooks/use-auth'; import { Cpu, LogOut, CheckCircle, Clock, Menu, X, Sun, Moon, ChevronRight, ChevronLeft, Settings, Globe, Video } from 'lucide-react'; import { motion, AnimatePresence } from 'framer-motion'; +import { ChatDrawer } from '../../components/ui/ChatDrawer'; export const ClientLayout: React.FC = () => { const { user, logout } = useAuthStore(); @@ -394,6 +395,8 @@ export const ClientLayout: React.FC = () => { )} + {/* AI Assistant Chat Drawer */} + ); }; diff --git a/Channel-Frontend/src/components/ui/ChatDrawer.tsx b/Channel-Frontend/src/components/ui/ChatDrawer.tsx new file mode 100644 index 0000000..21a865e --- /dev/null +++ b/Channel-Frontend/src/components/ui/ChatDrawer.tsx @@ -0,0 +1,569 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { useNavigate, useLocation } from 'react-router-dom'; +import { motion, AnimatePresence } from 'framer-motion'; +import { Sparkles, X, Send, Bot, User as UserIcon, FileText, Minimize2, Maximize2, RefreshCw, Compass, Eye, Plus, History, MessageSquare } from 'lucide-react'; +import { axiosInstance } from '../../services/axios'; +import { useAuthStore } from '../../hooks/use-auth'; +import { AssetViewerModal } from '../../features/assets/components/AssetViewerModal'; +import MarkdownViewer from './MarkdownViewer'; +import type { Asset } from '../../types/assets'; + +export interface CitationItem { + assetId: string; + title: string; + location: string; + type: string; + isRecommended?: boolean; +} + +export interface ChatMessage { + id?: string; + sender: 'USER' | 'ASSISTANT'; + content: string; + citations?: CitationItem[]; + createdAt?: string; +} + +export interface ChatSessionItem { + id: string; + createdAt: string; + updatedAt: string; + messages?: ChatMessage[]; +} + +export const ChatDrawer: React.FC = () => { + const navigate = useNavigate(); + const location = useLocation(); + const { user } = useAuthStore(); + + const [isOpen, setIsOpen] = useState(false); + const [isMinimized, setIsMinimized] = useState(false); + const [drawerSize, setDrawerSize] = useState<'standard' | 'wide' | 'maximized'>('standard'); + const [showHistory, setShowHistory] = useState(false); + + const [prompt, setPrompt] = useState(''); + const [loading, setLoading] = useState(false); + const [sessionId, setSessionId] = useState(undefined); + const [pastSessions, setPastSessions] = useState([]); + const [loadingHistory, setLoadingHistory] = useState(false); + + const [messages, setMessages] = useState([ + { + sender: 'ASSISTANT', + content: 'Hello! I am your **Tech4Biz AI Advisor** powered by DeepSeek. Ask me anything about our enterprise catalog assets, compliance standards, NDA agreements, or ecosystem offerings.', + } + ]); + const [activePreviewAsset, setActivePreviewAsset] = useState(null); + + const messagesEndRef = useRef(null); + + useEffect(() => { + if (isOpen) { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + } + }, [messages, isOpen]); + + // Load user session history when history drawer is opened + const fetchSessionHistory = async () => { + setLoadingHistory(true); + try { + const res = await axiosInstance.get('/chat/history'); + setPastSessions(res.data || []); + } catch (err) { + console.error('Failed to fetch chat history:', err); + } finally { + setLoadingHistory(false); + } + }; + + const handleToggleHistory = () => { + if (!showHistory) { + fetchSessionHistory(); + } + setShowHistory(!showHistory); + }; + + const handleStartNewChat = async () => { + setLoading(true); + try { + const res = await axiosInstance.post('/chat/sessions'); + setSessionId(res.data.sessionId); + setMessages([ + { + sender: 'ASSISTANT', + content: 'Started a new session! I am your **Tech4Biz AI Advisor**. Ask me anything about your catalog documents, NDA agreements, or platform features.', + } + ]); + setShowHistory(false); + } catch (err) { + console.error('Failed to create new session', err); + } finally { + setLoading(false); + } + }; + + const handleLoadSession = async (sessId: string) => { + setLoading(true); + try { + const res = await axiosInstance.get(`/chat/sessions/${sessId}`); + setSessionId(sessId); + const loadedMessages = (res.data.messages || []).map((m: any) => ({ + id: m.id, + sender: m.sender, + content: m.content, + citations: m.citations || [], + createdAt: m.createdAt, + })); + setMessages(loadedMessages.length > 0 ? loadedMessages : [ + { + sender: 'ASSISTANT', + content: 'Loaded past session. Ask your follow up questions below!', + } + ]); + setShowHistory(false); + } catch (err) { + console.error('Failed to load session messages', err); + } finally { + setLoading(false); + } + }; + + const handleSend = async (customPrompt?: string) => { + const textToSend = customPrompt || prompt; + if (!textToSend.trim() || loading) return; + + const userMsg: ChatMessage = { + sender: 'USER', + content: textToSend, + createdAt: new Date().toISOString(), + }; + + setMessages(prev => [...prev, userMsg]); + if (!customPrompt) setPrompt(''); + setLoading(true); + + try { + const response = await axiosInstance.post('/chat/query', { + prompt: textToSend, + sessionId, + }); + + const { sessionId: newSessionId, message } = response.data; + setSessionId(newSessionId); + + setMessages(prev => [...prev, { + id: message.id, + sender: 'ASSISTANT', + content: message.content, + citations: message.citations || [], + createdAt: message.createdAt, + }]); + } catch (err: any) { + console.error('Chat API Error:', err); + setMessages(prev => [...prev, { + sender: 'ASSISTANT', + content: 'Sorry, I encountered an issue connecting to the AI Gateway. Please try again.', + }]); + } finally { + setLoading(false); + } + }; + + const openAssetPreview = async (cite: CitationItem) => { + try { + if (cite.location === 'Featured Content Showcase') { + const showcaseRes = await axiosInstance.get('/showcase'); + const showcaseItem = showcaseRes.data.find((item: any) => item.id === cite.assetId || item.title === cite.title); + if (showcaseItem) { + setActivePreviewAsset({ + id: showcaseItem.id, + title: showcaseItem.title, + type: 'case_study', + url: showcaseItem.youtubeUrl || showcaseItem.mediaUrl || 'https://www.youtube.com/watch?v=svJDGYlQLYw', + description: showcaseItem.description || `Verified Showcase Reel: ${showcaseItem.title}`, + isDownloadable: false, + createdAt: showcaseItem.createdAt || new Date().toISOString(), + updatedAt: new Date().toISOString(), + categoryId: null, + organizationId: null, + } as any); + return; + } + } + + const res = await axiosInstance.get(`/assets/${cite.assetId}`); + setActivePreviewAsset(res.data.asset); + } catch (err) { + console.error('Failed to fetch asset from API, constructing fallback preview:', err); + let videoUrl = ''; + if (cite.title.toLowerCase().includes('digital twin') || cite.title.toLowerCase().includes('digitaltwin')) { + videoUrl = 'https://www.youtube.com/watch?v=svJDGYlQLYw'; + } else if (cite.title.toLowerCase().includes('surveillance')) { + videoUrl = 'https://www.youtube.com/watch?v=svJDGYlQLYw'; + } + + setActivePreviewAsset({ + id: cite.assetId, + title: cite.title, + type: cite.type || 'case_study', + url: videoUrl, + description: `Verified Document Source: ${cite.title} (${cite.location})`, + isDownloadable: false, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + categoryId: null, + organizationId: null, + } as any); + } + }; + + const handleGoToAsset = (cite: CitationItem) => { + setIsMinimized(true); + const typeLower = (cite.type || '').toLowerCase(); + const titleLower = (cite.title || '').toLowerCase(); + const isCurrentAdmin = location.pathname.startsWith('/admin') || user?.role === 'ADMIN'; + + if (typeLower.includes('case_study') || titleLower.includes('case study') || typeLower.includes('video')) { + navigate(isCurrentAdmin ? '/admin/showcase' : '/client/showcase', { state: { highlightAssetId: cite.assetId } }); + } else if (titleLower.includes('nda') || titleLower.includes('msa') || titleLower.includes('agreement') || typeLower.includes('legal')) { + navigate(isCurrentAdmin ? '/admin/legal' : '/client/agreements', { state: { highlightAssetId: cite.assetId } }); + } else if (typeLower.includes('ecosystem') || titleLower.includes('offering')) { + navigate(isCurrentAdmin ? '/admin/ecosystem' : '/client/ecosystem', { state: { highlightAssetId: cite.assetId } }); + } else { + navigate(isCurrentAdmin ? '/admin/assets' : '/client/assets', { state: { highlightAssetId: cite.assetId } }); + } + }; + + const cycleSize = () => { + if (drawerSize === 'standard') setDrawerSize('wide'); + else if (drawerSize === 'wide') setDrawerSize('maximized'); + else setDrawerSize('standard'); + }; + + const getDimensions = () => { + if (isMinimized) return { width: '380px', height: '56px' }; + if (drawerSize === 'maximized') return { width: '860px', height: '82vh' }; + if (drawerSize === 'wide') return { width: '640px', height: '680px' }; + return { width: '440px', height: '580px' }; + }; + + const dimensions = getDimensions(); + + return ( + <> + {/* Floating Trigger Pill */} + {!isOpen && ( + { setIsOpen(true); setIsMinimized(false); }} + className="fixed bottom-6 right-6 z-50 flex items-center gap-2.5 px-4 py-3 bg-slate-900 text-white rounded-full shadow-2xl border border-slate-700/80 hover:border-emerald-500/80 transition-all cursor-pointer group" + > +
+ +
+ + AI Advisor + + + + + +
+ )} + + {/* Floating Glassmorphic Drawer Window */} + + {isOpen && ( + + {/* Window Header (Clean Layout) */} +
+
+
+ +
+
+

Tech4Biz AI Advisor

+

Powered by DeepSeek LLM Engine

+
+
+ +
+ {/* + New Chat Button */} + {!isMinimized && ( + + )} + + {/* Session History Toggle Button */} + {!isMinimized && ( + + )} + + {/* Resize Drawer Cycle Button */} + {!isMinimized && ( + + )} + + {/* Minimize Button */} + + + {/* Close Button */} + +
+
+ + {/* Past Session History Drawer Side Panel */} + {showHistory && !isMinimized && ( +
+
+ Saved Past Sessions + {pastSessions.length} sessions +
+ {loadingHistory ? ( +
+ Loading chat history... +
+ ) : pastSessions.length === 0 ? ( +
+ No past sessions found. Start a conversation! +
+ ) : ( + pastSessions.map((sess) => { + const firstUserMsg = sess.messages?.find(m => m.sender === 'USER')?.content || 'Chat Session'; + const isActive = sess.id === sessionId; + return ( +
handleLoadSession(sess.id)} + className={`p-2 rounded-xl border text-xs cursor-pointer flex items-center justify-between transition-all ${ + isActive + ? 'bg-emerald-500/20 text-emerald-300 border-emerald-500/40' + : 'bg-slate-950/70 hover:bg-slate-800 text-slate-300 border-slate-800' + }`} + > +
+ + {firstUserMsg} +
+ + {new Date(sess.updatedAt || sess.createdAt).toLocaleDateString()} + +
+ ); + }) + )} +
+ )} + + {/* Content Body (Visible when not minimized) */} + {!isMinimized && ( + <> + {/* Messages Stream */} +
+ {messages.map((msg, i) => ( +
+
+ {msg.sender === 'USER' ? : } +
+ +
+
+ {msg.sender === 'USER' ? ( + msg.content + ) : ( + + )} +
+ + {/* Citation Cards with Direct Preview & Location Redirection */} + {msg.citations && msg.citations.length > 0 && ( +
+

+ Verified Knowledge Sources ({msg.citations.length}): +

+
+ {msg.citations.map((cite) => ( +
+
+
+ +
+
+ + {cite.title} + + {cite.isRecommended && ( + + ⭐ Recommended + + )} +
+ + {cite.location} + +
+
+
+ + {/* Action Buttons: Preview & Go to Location */} +
+ + + +
+
+ ))} +
+
+ )} +
+
+ ))} + + {loading && ( +
+ + Analyzing catalog knowledge with DeepSeek... +
+ )} + +
+
+ + {/* Quick Suggestion Chips */} +
+ + + +
+ + {/* Input Controls */} +
+ setPrompt(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleSend()} + placeholder="Ask about assets, NDA agreements, or portal features..." + disabled={loading} + className="flex-1 bg-slate-950 border border-slate-800 rounded-xl px-3.5 py-2 text-xs text-white placeholder-slate-500 focus:outline-none focus:border-emerald-500/60 transition-all font-sans" + /> + +
+ + )} + + )} + + + {/* Asset Preview Modal Triggered from Citation Click */} + {activePreviewAsset && ( + {}} + onClose={() => setActivePreviewAsset(null)} + /> + )} + + ); +}; diff --git a/Channel-Frontend/src/components/ui/MarkdownViewer.tsx b/Channel-Frontend/src/components/ui/MarkdownViewer.tsx index 52119fa..5c57587 100644 --- a/Channel-Frontend/src/components/ui/MarkdownViewer.tsx +++ b/Channel-Frontend/src/components/ui/MarkdownViewer.tsx @@ -1,404 +1,414 @@ import React from "react"; interface MarkdownBlock { - type: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "code" | "blockquote" | "ul" | "ol" | "hr" | "p"; - content: string; - language?: string; - items?: string[]; + type: "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "code" | "blockquote" | "ul" | "ol" | "hr" | "p"; + content: string; + language?: string; + items?: string[]; } interface ParseState { - blocks: MarkdownBlock[]; - currentCodeBlock: { language: string; lines: string[] } | null; - currentList: { type: "ul" | "ol"; items: string[] } | null; - currentParagraphLines: string[]; + blocks: MarkdownBlock[]; + currentCodeBlock: { language: string; lines: string[] } | null; + currentList: { type: "ul" | "ol"; items: string[] } | null; + currentParagraphLines: string[]; } interface InlineToken { - type: "text" | "bold" | "italic" | "code" | "link"; - text: string; - url?: string; + type: "text" | "bold" | "italic" | "code" | "link"; + text: string; + url?: string; } const flushParagraph = (state: ParseState): void => { - if (state.currentParagraphLines.length > 0) { - state.blocks.push({ - type: "p", - content: state.currentParagraphLines.join(" ").trim(), - }); - state.currentParagraphLines = []; - } + if (state.currentParagraphLines.length > 0) { + state.blocks.push({ + type: "p", + content: state.currentParagraphLines.join(" ").trim(), + }); + state.currentParagraphLines = []; + } }; const flushList = (state: ParseState): void => { - if (state.currentList) { - state.blocks.push({ - type: state.currentList.type, - content: "", - items: state.currentList.items, - }); - state.currentList = null; - } + if (state.currentList) { + state.blocks.push({ + type: state.currentList.type, + content: "", + items: state.currentList.items, + }); + state.currentList = null; + } }; const handleCodeBlock = (trimmed: string, state: ParseState): boolean => { - if (trimmed.startsWith("```")) { - if (state.currentCodeBlock) { - state.blocks.push({ - type: "code", - content: state.currentCodeBlock.lines.join("\n"), - language: state.currentCodeBlock.language, - }); - state.currentCodeBlock = null; - } else { - flushParagraph(state); - flushList(state); - const language = trimmed.slice(3).trim(); - state.currentCodeBlock = { language, lines: [] }; - } - return true; + if (trimmed.startsWith("```")) { + if (state.currentCodeBlock) { + state.blocks.push({ + type: "code", + content: state.currentCodeBlock.lines.join("\n"), + language: state.currentCodeBlock.language, + }); + state.currentCodeBlock = null; + } else { + flushParagraph(state); + flushList(state); + const language = trimmed.slice(3).trim(); + state.currentCodeBlock = { language, lines: [] }; } - return false; + return true; + } + return false; }; const handleHeading = (line: string, state: ParseState): boolean => { - const match = line.match(/^(#{1,6})\s+(.*)$/); - if (match) { - flushParagraph(state); - flushList(state); - const level = match[1].length; - state.blocks.push({ - type: `h${level}` as any, - content: match[2].trim(), - }); - return true; - } - return false; + const match = line.match(/^(#{1,6})\s+(.*)$/); + if (match) { + flushParagraph(state); + flushList(state); + const level = match[1].length; + state.blocks.push({ + type: `h${level}` as any, + content: match[2].trim(), + }); + return true; + } + return false; }; const handleBlockquote = (trimmed: string, state: ParseState): boolean => { - if (trimmed.startsWith(">")) { - flushParagraph(state); - flushList(state); - state.blocks.push({ - type: "blockquote", - content: trimmed.replace(/^>\s*/, ""), - }); - return true; - } - return false; + if (trimmed.startsWith(">")) { + flushParagraph(state); + flushList(state); + state.blocks.push({ + type: "blockquote", + content: trimmed.replace(/^>\s*/, ""), + }); + return true; + } + return false; }; const handleLists = (line: string, state: ParseState): boolean => { - const ulMatch = line.match(/^(\s*)([-*+])\s+(.*)$/); - if (ulMatch) { - flushParagraph(state); - const content = ulMatch[3].trim(); - if (state.currentList && state.currentList.type === "ul") { - state.currentList.items.push(content); - } else { - flushList(state); - state.currentList = { type: "ul", items: [content] }; - } - return true; + const ulMatch = line.match(/^(\s*)([-*+])\s+(.*)$/); + if (ulMatch) { + flushParagraph(state); + const content = ulMatch[3].trim(); + if (state.currentList && state.currentList.type === "ul") { + state.currentList.items.push(content); + } else { + flushList(state); + state.currentList = { type: "ul", items: [content] }; } + return true; + } - const olMatch = line.match(/^(\s*)(\d+)\.\s+(.*)$/); - if (olMatch) { - flushParagraph(state); - const content = olMatch[3].trim(); - if (state.currentList && state.currentList.type === "ol") { - state.currentList.items.push(content); - } else { - flushList(state); - state.currentList = { type: "ol", items: [content] }; - } - return true; + const olMatch = line.match(/^(\s*)(\d+)\.\s+(.*)$/); + if (olMatch) { + flushParagraph(state); + const content = olMatch[3].trim(); + if (state.currentList && state.currentList.type === "ol") { + state.currentList.items.push(content); + } else { + flushList(state); + state.currentList = { type: "ol", items: [content] }; } + return true; + } - return false; + return false; }; const handleLine = (line: string, state: ParseState): void => { - const trimmed = line.trim(); + const trimmed = line.trim(); - if (handleCodeBlock(trimmed, state)) { - return; - } + if (handleCodeBlock(trimmed, state)) { + return; + } - if (state.currentCodeBlock) { - state.currentCodeBlock.lines.push(line); - return; - } - - if (trimmed === "---" || trimmed === "***" || trimmed === "___") { - flushParagraph(state); - flushList(state); - state.blocks.push({ type: "hr", content: "" }); - return; - } - - if (handleHeading(line, state) || handleBlockquote(trimmed, state)) { - return; - } - - if (handleLists(line, state)) { - return; - } - - if (trimmed === "") { - flushParagraph(state); - flushList(state); - return; - } + if (state.currentCodeBlock) { + state.currentCodeBlock.lines.push(line); + return; + } + if (trimmed === "---" || trimmed === "***" || trimmed === "___") { + flushParagraph(state); flushList(state); - state.currentParagraphLines.push(line); + state.blocks.push({ type: "hr", content: "" }); + return; + } + + if (handleHeading(line, state) || handleBlockquote(trimmed, state)) { + return; + } + + if (handleLists(line, state)) { + return; + } + + if (trimmed === "") { + flushParagraph(state); + flushList(state); + return; + } + + flushList(state); + state.currentParagraphLines.push(line); }; export const parseMarkdown = (text: string): MarkdownBlock[] => { - const lines = text.split("\n"); - const state: ParseState = { - blocks: [], - currentCodeBlock: null, - currentList: null, - currentParagraphLines: [], - }; + const lines = text.split("\n"); + const state: ParseState = { + blocks: [], + currentCodeBlock: null, + currentList: null, + currentParagraphLines: [], + }; - for (let i = 0; i < lines.length; i++) { - handleLine(lines[i], state); - } + for (let i = 0; i < lines.length; i++) { + handleLine(lines[i], state); + } - flushParagraph(state); - flushList(state); + flushParagraph(state); + flushList(state); - return state.blocks; + return state.blocks; }; const parseInlineLinks = (tokens: InlineToken[]): InlineToken[] => { - const updated: InlineToken[] = []; - for (const part of tokens) { - if (part.type === "text") { - const regex = /\[([^\]]+)\]\(([^)]+)\)/g; - let lastIndex = 0; - let match; - while ((match = regex.exec(part.text)) !== null) { - const before = part.text.substring(lastIndex, match.index); - if (before) updated.push({ type: "text", text: before }); - updated.push({ type: "link", text: match[1], url: match[2] }); - lastIndex = regex.lastIndex; - } - const after = part.text.substring(lastIndex); - if (after) updated.push({ type: "text", text: after }); - } else { - updated.push(part); - } + const updated: InlineToken[] = []; + for (const part of tokens) { + if (part.type === "text") { + const regex = /\[([^\]]+)\]\(([^)]+)\)/g; + let lastIndex = 0; + let match; + while ((match = regex.exec(part.text)) !== null) { + const before = part.text.substring(lastIndex, match.index); + if (before) updated.push({ type: "text", text: before }); + updated.push({ type: "link", text: match[1], url: match[2] }); + lastIndex = regex.lastIndex; + } + const after = part.text.substring(lastIndex); + if (after) updated.push({ type: "text", text: after }); + } else { + updated.push(part); } - return updated; + } + return updated; }; const parseInlineUrls = (tokens: InlineToken[]): InlineToken[] => { - const updated: InlineToken[] = []; - for (const part of tokens) { - if (part.type === "text") { - const regex = /(https?:\/\/[^\s)]+)/g; - let lastIndex = 0; - let match; - while ((match = regex.exec(part.text)) !== null) { - const before = part.text.substring(lastIndex, match.index); - if (before) updated.push({ type: "text", text: before }); - updated.push({ type: "link", text: match[1], url: match[1] }); - lastIndex = regex.lastIndex; - } - const after = part.text.substring(lastIndex); - if (after) updated.push({ type: "text", text: after }); - } else { - updated.push(part); - } + const updated: InlineToken[] = []; + for (const part of tokens) { + if (part.type === "text") { + const regex = /(https?:\/\/[^\s)]+)/g; + let lastIndex = 0; + let match; + while ((match = regex.exec(part.text)) !== null) { + const before = part.text.substring(lastIndex, match.index); + if (before) updated.push({ type: "text", text: before }); + updated.push({ type: "link", text: match[1], url: match[1] }); + lastIndex = regex.lastIndex; + } + const after = part.text.substring(lastIndex); + if (after) updated.push({ type: "text", text: after }); + } else { + updated.push(part); } - return updated; + } + return updated; }; const parseInlineBold = (tokens: InlineToken[]): InlineToken[] => { - const updated: InlineToken[] = []; - for (const part of tokens) { - if (part.type === "text") { - const regex = /\*\*([^*]+)\*\*/g; - let lastIndex = 0; - let match; - while ((match = regex.exec(part.text)) !== null) { - const before = part.text.substring(lastIndex, match.index); - if (before) updated.push({ type: "text", text: before }); - updated.push({ type: "bold", text: match[1] }); - lastIndex = regex.lastIndex; - } - const after = part.text.substring(lastIndex); - if (after) updated.push({ type: "text", text: after }); - } else { - updated.push(part); - } + const updated: InlineToken[] = []; + for (const part of tokens) { + if (part.type === "text") { + const regex = /\*\*([^*]+)\*\*/g; + let lastIndex = 0; + let match; + while ((match = regex.exec(part.text)) !== null) { + const before = part.text.substring(lastIndex, match.index); + if (before) updated.push({ type: "text", text: before }); + updated.push({ type: "bold", text: match[1] }); + lastIndex = regex.lastIndex; + } + const after = part.text.substring(lastIndex); + if (after) updated.push({ type: "text", text: after }); + } else { + updated.push(part); } - return updated; + } + return updated; }; const parseInlineCode = (tokens: InlineToken[]): InlineToken[] => { - const updated: InlineToken[] = []; - for (const part of tokens) { - if (part.type === "text") { - const regex = /`([^`]+)`/g; - let lastIndex = 0; - let match; - while ((match = regex.exec(part.text)) !== null) { - const before = part.text.substring(lastIndex, match.index); - if (before) updated.push({ type: "text", text: before }); - updated.push({ type: "code", text: match[1] }); - lastIndex = regex.lastIndex; - } - const after = part.text.substring(lastIndex); - if (after) updated.push({ type: "text", text: after }); - } else { - updated.push(part); - } + const updated: InlineToken[] = []; + for (const part of tokens) { + if (part.type === "text") { + const regex = /`([^`]+)`/g; + let lastIndex = 0; + let match; + while ((match = regex.exec(part.text)) !== null) { + const before = part.text.substring(lastIndex, match.index); + if (before) updated.push({ type: "text", text: before }); + updated.push({ type: "code", text: match[1] }); + lastIndex = regex.lastIndex; + } + const after = part.text.substring(lastIndex); + if (after) updated.push({ type: "text", text: after }); + } else { + updated.push(part); } - return updated; + } + return updated; }; const parseInlineItalic = (tokens: InlineToken[]): InlineToken[] => { - const updated: InlineToken[] = []; - for (const part of tokens) { - if (part.type === "text") { - const regex = /\*([^*]+)\*/g; - let lastIndex = 0; - let match; - while ((match = regex.exec(part.text)) !== null) { - const before = part.text.substring(lastIndex, match.index); - if (before) updated.push({ type: "text", text: before }); - updated.push({ type: "italic", text: match[1] }); - lastIndex = regex.lastIndex; - } - const after = part.text.substring(lastIndex); - if (after) updated.push({ type: "text", text: after }); - } else { - updated.push(part); - } + const updated: InlineToken[] = []; + for (const part of tokens) { + if (part.type === "text") { + const regex = /\*([^*]+)\*/g; + let lastIndex = 0; + let match; + while ((match = regex.exec(part.text)) !== null) { + const before = part.text.substring(lastIndex, match.index); + if (before) updated.push({ type: "text", text: before }); + updated.push({ type: "italic", text: match[1] }); + lastIndex = regex.lastIndex; + } + const after = part.text.substring(lastIndex); + if (after) updated.push({ type: "text", text: after }); + } else { + updated.push(part); } - return updated; + } + return updated; }; -export const renderInlineText = (text: string): React.ReactNode[] => { - if (!text) return []; +export const renderInlineText = (text: string, isDark: boolean = false): React.ReactNode[] => { + if (!text) return []; - let tokens: InlineToken[] = [{ type: "text", text }]; - tokens = parseInlineLinks(tokens); - tokens = parseInlineUrls(tokens); - tokens = parseInlineBold(tokens); - tokens = parseInlineCode(tokens); - tokens = parseInlineItalic(tokens); + let tokens: InlineToken[] = [{ type: "text", text }]; + tokens = parseInlineLinks(tokens); + tokens = parseInlineUrls(tokens); + tokens = parseInlineBold(tokens); + tokens = parseInlineCode(tokens); + tokens = parseInlineItalic(tokens); - return tokens.map((part, idx) => { - switch (part.type) { - case "bold": - return {part.text}; - case "italic": - return {part.text}; - case "code": - return {part.text}; - case "link": - return ( - - {part.text} - - ); - default: - return {part.text}; - } - }); -}; - -const renderListBlock = (block: MarkdownBlock, key: string): React.ReactNode => { - const Component = block.type === "ul" ? "ul" : "ol"; - const listClass = block.type === "ul" - ? "list-disc pl-6 space-y-1.5 my-2.5 text-sm text-ink-800" - : "list-decimal pl-6 space-y-1.5 my-2.5 text-sm text-ink-800"; - return ( - - {block.items?.map((item, idx) => ( -
  • {renderInlineText(item)}
  • - ))} -
    - ); -}; - -const renderHeadingBlock = (block: MarkdownBlock, key: string): React.ReactNode => { - const level = block.type.slice(1); - const classes: Record = { - "1": "text-2xl sm:text-3xl font-extrabold text-ink-900 mt-6 mb-3 border-b border-ink-150 pb-2", - "2": "text-xl sm:text-2xl font-extrabold text-ink-900 mt-5 mb-2.5", - "3": "text-lg sm:text-xl font-bold text-ink-900 mt-4 mb-2", - "4": "text-base sm:text-lg font-bold text-ink-800 mt-3 mb-1.5", - "5": "text-sm sm:text-base font-bold text-ink-800 mt-2.5 mb-1.5", - "6": "text-xs sm:text-sm font-bold text-ink-700 mt-2 mb-1", - }; - const Component = block.type as any; - return ( - - {renderInlineText(block.content)} - - ); -}; - -const renderBlock = (block: MarkdownBlock, index: number): React.ReactNode => { - const key = `${block.type}-${index}`; - if (block.type.startsWith("h") && block.type.length === 2 && block.type !== "hr") { - return renderHeadingBlock(block, key); - } - switch (block.type) { - case "blockquote": - return ( -
    - {renderInlineText(block.content)} -
    - ); - case "ul": - case "ol": - return renderListBlock(block, key); - case "code": - return ( -
    - {block.language && ( -
    - {block.language} -
    - )} -
    -                        {block.content}
    -                    
    -
    - ); - case "hr": - return
    ; - default: - return

    {renderInlineText(block.content)}

    ; + return tokens.map((part, idx) => { + switch (part.type) { + case "bold": + return {part.text}; + case "italic": + return {part.text}; + case "code": + return ( + + {part.text} + + ); + case "link": + return ( + + {part.text} + + ); + default: + return {part.text}; } + }); +}; + +const renderListBlock = (block: MarkdownBlock, key: string, isDark: boolean): React.ReactNode => { + const Component = block.type === "ul" ? "ul" : "ol"; + const listClass = block.type === "ul" + ? `list-disc pl-5 space-y-1.5 my-2 text-xs ${isDark ? 'text-slate-100' : 'text-ink-800'}` + : `list-decimal pl-5 space-y-1.5 my-2 text-xs ${isDark ? 'text-slate-100' : 'text-ink-800'}`; + return ( + + {block.items?.map((item, idx) => ( +
  • {renderInlineText(item, isDark)}
  • + ))} +
    + ); +}; + +const renderHeadingBlock = (block: MarkdownBlock, key: string, isDark: boolean): React.ReactNode => { + const level = block.type.slice(1); + const classes: Record = { + "1": `text-lg sm:text-xl font-extrabold ${isDark ? 'text-white' : 'text-ink-900'} mt-4 mb-2 border-b border-slate-700/50 pb-1`, + "2": `text-base sm:text-lg font-extrabold ${isDark ? 'text-white' : 'text-ink-900'} mt-3 mb-1.5`, + "3": `text-sm sm:text-base font-bold ${isDark ? 'text-emerald-400' : 'text-ink-900'} mt-2.5 mb-1`, + "4": `text-xs sm:text-sm font-bold ${isDark ? 'text-emerald-300' : 'text-ink-800'} mt-2 mb-1`, + "5": `text-xs font-bold ${isDark ? 'text-slate-200' : 'text-ink-800'} mt-1.5 mb-1`, + "6": `text-xs font-bold ${isDark ? 'text-slate-300' : 'text-ink-700'} mt-1 mb-0.5`, + }; + const Component = block.type as any; + return ( + + {renderInlineText(block.content, isDark)} + + ); +}; + +const renderBlock = (block: MarkdownBlock, index: number, isDark: boolean): React.ReactNode => { + const key = `${block.type}-${index}`; + if (block.type.startsWith("h") && block.type.length === 2 && block.type !== "hr") { + return renderHeadingBlock(block, key, isDark); + } + switch (block.type) { + case "blockquote": + return ( +
    + {renderInlineText(block.content, isDark)} +
    + ); + case "ul": + case "ol": + return renderListBlock(block, key, isDark); + case "code": + return ( +
    + {block.language && ( +
    + {block.language} +
    + )} +
    +            {block.content}
    +          
    +
    + ); + case "hr": + return
    ; + default: + return ( +

    + {renderInlineText(block.content, isDark)} +

    + ); + } }; export interface MarkdownViewerProps { - markdown: string; + markdown: string; + variant?: 'light' | 'dark' | 'auto'; } -export const MarkdownViewer: React.FC = ({ markdown }) => { - const blocks = parseMarkdown(markdown); - return ( -
    - {blocks.map((block, idx) => renderBlock(block, idx))} -
    - ); +export const MarkdownViewer: React.FC = ({ markdown, variant = 'auto' }) => { + const blocks = parseMarkdown(markdown); + const isDark = variant === 'dark'; + return ( +
    + {blocks.map((block, idx) => renderBlock(block, idx, isDark))} +
    + ); }; export default MarkdownViewer; diff --git a/Channel-Frontend/src/features/assets/components/AssetCard.tsx b/Channel-Frontend/src/features/assets/components/AssetCard.tsx index 5be2c3f..4fe952a 100644 --- a/Channel-Frontend/src/features/assets/components/AssetCard.tsx +++ b/Channel-Frontend/src/features/assets/components/AssetCard.tsx @@ -145,6 +145,7 @@ export const AssetCard: React.FC = ({ return ( { diff --git a/Channel-Frontend/src/features/assets/components/AssetViewerModal.tsx b/Channel-Frontend/src/features/assets/components/AssetViewerModal.tsx index 6ffdbe9..57f7218 100644 --- a/Channel-Frontend/src/features/assets/components/AssetViewerModal.tsx +++ b/Channel-Frontend/src/features/assets/components/AssetViewerModal.tsx @@ -1,5 +1,6 @@ import React, { useState, useEffect } from 'react'; -import { Minimize2, Maximize2, Globe, ExternalLink, FileText, File, Download } from 'lucide-react'; +import { useNavigate } from 'react-router-dom'; +import { Minimize2, Maximize2, Globe, ExternalLink, FileText, File, Download, Compass, Sparkles } from 'lucide-react'; import { axiosInstance } from '../../../services/axios'; import type { Asset } from '../../../types/assets'; import type { User } from '../../../types/auth'; @@ -23,6 +24,7 @@ export const AssetViewerModal: React.FC = ({ user, onDownload }) => { + const navigate = useNavigate(); const [isMaximized, setIsMaximized] = useState(false); const [isLoadingText, setIsLoadingText] = useState(false); const [textPreviewContent, setTextPreviewContent] = useState(''); @@ -31,6 +33,29 @@ export const AssetViewerModal: React.FC = ({ const [isLoadingDoc, setIsLoadingDoc] = useState(false); const [docLoadError, setDocLoadError] = useState(false); const wordContainerRef = React.useRef(null); + const extractYouTubeVideoId = (url: string): string | null => { + try { + const urlObj = new URL(url); + if (urlObj.hostname.includes('youtu.be')) { + return urlObj.pathname.slice(1).split(/[?#]/)[0]; + } + if (urlObj.pathname.includes('/shorts/') || urlObj.pathname.includes('/embed/')) { + const parts = urlObj.pathname.split('/'); + return parts.pop()?.split(/[?#]/)[0] || null; + } + if (urlObj.searchParams.has('v')) { + return urlObj.searchParams.get('v'); + } + } catch (e) {} + const patterns = [ + /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/|youtube\.com\/shorts\/)([a-zA-Z0-9_-]{11})/, + ]; + for (const pattern of patterns) { + const match = url.match(pattern); + if (match) return match[1]; + } + return null; + }; const getFullAssetUrl = (url: string) => { let resolvedUrl = url; @@ -238,8 +263,8 @@ export const AssetViewerModal: React.FC = ({ } }, [docBlob, isWord]); - const formatBytes = (bytes: number, decimals = 2) => { - if (bytes === 0) return '0 Bytes'; + const formatBytes = (bytes?: number, decimals = 2) => { + if (!bytes || isNaN(bytes) || bytes === 0) return ''; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['Bytes', 'KB', 'MB', 'GB']; @@ -252,6 +277,29 @@ export const AssetViewerModal: React.FC = ({ onClose(); }; + const handleLocateInCatalog = () => { + onClose(); + if (!asset) return; + + const typeLower = (asset.type || '').toLowerCase(); + const titleLower = (asset.title || '').toLowerCase(); + const isCurrentAdmin = location.pathname.startsWith('/admin') || user?.role === 'ADMIN'; + + let targetBasePath = ''; + if (typeLower.includes('case_study') || titleLower.includes('case study') || typeLower.includes('video')) { + targetBasePath = isCurrentAdmin ? '/admin/showcase' : '/client/showcase'; + } else if (titleLower.includes('nda') || titleLower.includes('msa') || titleLower.includes('agreement') || typeLower.includes('legal')) { + targetBasePath = isCurrentAdmin ? '/admin/legal' : '/client/agreements'; + } else if (typeLower.includes('ecosystem') || titleLower.includes('offering')) { + targetBasePath = isCurrentAdmin ? '/admin/ecosystem' : '/client/ecosystem'; + } else { + targetBasePath = isCurrentAdmin ? '/admin/assets' : '/client/assets'; + } + + const fullUrl = `${targetBasePath}?highlight=${asset.id}`; + navigate(fullUrl, { state: { highlightAssetId: asset.id } }); + }; + return ( = ({
    {asset?.title} - {asset?.type === 'url' ? 'External Web Link' : `${asset?.type} • ${asset ? formatBytes(asset.size) : ''}`} + {asset?.type === 'url' ? 'External Web Link' : asset?.size ? `${asset.type} • ${formatBytes(asset.size)}` : asset?.type}
    )} - {asset && asset.type !== 'url' && (user?.role === 'ADMIN' || asset.isDownloadable || asset.downloadRequests?.[0]?.status === 'APPROVED') && ( + {asset && ( + + )} + + {asset && asset.type !== 'url' && asset.type !== 'case_study' && (user?.role === 'ADMIN' || asset.isDownloadable || asset.downloadRequests?.[0]?.status === 'APPROVED') && (
    + ) : asset.type === 'case_study' || asset.type.includes('video') || asset.type === 'showcase' ? ( +
    +
    + + + Executive Case Study & Video Showcase + + {asset.title} +
    + +
    + {asset.url && (asset.url.includes('youtube.com') || asset.url.includes('youtu.be')) ? ( +
    +