Chatbot_highlight
This commit is contained in:
parent
a127b5ffc4
commit
67acafd245
736
Channel-Backend/package-lock.json
generated
736
Channel-Backend/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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())
|
||||
}
|
||||
|
||||
@ -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.' });
|
||||
|
||||
39
Channel-Backend/src/controllers/branch.controller.ts
Normal file
39
Channel-Backend/src/controllers/branch.controller.ts
Normal file
@ -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); }
|
||||
};
|
||||
}
|
||||
107
Channel-Backend/src/controllers/chat.controller.ts
Normal file
107
Channel-Backend/src/controllers/chat.controller.ts
Normal file
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
15
Channel-Backend/src/routes/branch.routes.ts
Normal file
15
Channel-Backend/src/routes/branch.routes.ts
Normal file
@ -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;
|
||||
16
Channel-Backend/src/routes/chat.routes.ts
Normal file
16
Channel-Backend/src/routes/chat.routes.ts
Normal file
@ -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;
|
||||
38
Channel-Backend/src/services/branch.service.ts
Normal file
38
Channel-Backend/src/services/branch.service.ts
Normal file
@ -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 }
|
||||
});
|
||||
}
|
||||
}
|
||||
415
Channel-Backend/src/services/chat.service.ts
Normal file
415
Channel-Backend/src/services/chat.service.ts
Normal file
@ -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<string, CitationItem>();
|
||||
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,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
285
Channel-Backend/src/services/extraction.service.ts
Normal file
285
Channel-Backend/src/services/extraction.service.ts
Normal file
@ -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<OKFChunk[]> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -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 = () => {
|
||||
</div>
|
||||
</footer>
|
||||
</main>
|
||||
<ChatDrawer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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 = () => {
|
||||
</div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{/* AI Assistant Chat Drawer */}
|
||||
<ChatDrawer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
569
Channel-Frontend/src/components/ui/ChatDrawer.tsx
Normal file
569
Channel-Frontend/src/components/ui/ChatDrawer.tsx
Normal file
@ -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<string | undefined>(undefined);
|
||||
const [pastSessions, setPastSessions] = useState<ChatSessionItem[]>([]);
|
||||
const [loadingHistory, setLoadingHistory] = useState(false);
|
||||
|
||||
const [messages, setMessages] = useState<ChatMessage[]>([
|
||||
{
|
||||
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<Asset | null>(null);
|
||||
|
||||
const messagesEndRef = useRef<HTMLDivElement>(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 && (
|
||||
<motion.button
|
||||
initial={{ scale: 0.8, opacity: 0 }}
|
||||
animate={{ scale: 1, opacity: 1 }}
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => { 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"
|
||||
>
|
||||
<div className="p-1.5 rounded-full bg-emerald-500/20 text-emerald-400 border border-emerald-500/40 group-hover:scale-110 transition-transform">
|
||||
<Sparkles className="w-4 h-4" />
|
||||
</div>
|
||||
<span className="text-xs font-extrabold tracking-wide font-sans">
|
||||
AI Advisor
|
||||
</span>
|
||||
<span className="flex h-2 w-2 relative">
|
||||
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
|
||||
<span className="relative inline-flex rounded-full h-2 w-2 bg-emerald-500"></span>
|
||||
</span>
|
||||
</motion.button>
|
||||
)}
|
||||
|
||||
{/* Floating Glassmorphic Drawer Window */}
|
||||
<AnimatePresence>
|
||||
{isOpen && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 40, scale: 0.95 }}
|
||||
animate={{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
height: dimensions.height,
|
||||
width: dimensions.width
|
||||
}}
|
||||
exit={{ opacity: 0, y: 40, scale: 0.95 }}
|
||||
transition={{ type: 'spring', damping: 25, stiffness: 220 }}
|
||||
className="fixed bottom-6 right-6 z-50 bg-slate-950/95 backdrop-blur-2xl border border-slate-800 rounded-3xl shadow-2xl flex flex-col overflow-hidden text-slate-100 font-sans"
|
||||
>
|
||||
{/* Window Header (Clean Layout) */}
|
||||
<div className="px-4 py-3 bg-slate-900/90 border-b border-slate-800 flex items-center justify-between shrink-0">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<div className="p-2 rounded-xl bg-emerald-500/20 text-emerald-400 border border-emerald-500/30">
|
||||
<Bot className="w-4 h-4" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-xs text-white">Tech4Biz AI Advisor</h3>
|
||||
<p className="text-[10px] text-slate-400">Powered by DeepSeek LLM Engine</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5">
|
||||
{/* + New Chat Button */}
|
||||
{!isMinimized && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleStartNewChat}
|
||||
title="Start New Chat Session"
|
||||
className="p-1.5 rounded-lg bg-emerald-500/20 text-emerald-300 hover:bg-emerald-500/30 border border-emerald-500/40 text-[10px] font-bold flex items-center gap-1 transition-all cursor-pointer mr-1"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
<span className="hidden sm:inline">New Chat</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Session History Toggle Button */}
|
||||
{!isMinimized && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleToggleHistory}
|
||||
title="Past Chat Sessions"
|
||||
className={`p-1.5 rounded-lg border transition-colors cursor-pointer ${
|
||||
showHistory
|
||||
? 'bg-emerald-500/30 text-emerald-300 border-emerald-500/50'
|
||||
: 'text-slate-400 hover:text-white hover:bg-slate-800 border-slate-800'
|
||||
}`}
|
||||
>
|
||||
<History className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Resize Drawer Cycle Button */}
|
||||
{!isMinimized && (
|
||||
<button
|
||||
onClick={cycleSize}
|
||||
title={`Current size: ${drawerSize.toUpperCase()}. Click to toggle window width.`}
|
||||
className="p-1.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 transition-colors"
|
||||
>
|
||||
{drawerSize === 'maximized' ? <Minimize2 className="w-3.5 h-3.5" /> : <Maximize2 className="w-3.5 h-3.5" />}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Minimize Button */}
|
||||
<button
|
||||
onClick={() => setIsMinimized(!isMinimized)}
|
||||
className="p-1.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 transition-colors"
|
||||
>
|
||||
{isMinimized ? <Maximize2 className="w-3.5 h-3.5" /> : <Minimize2 className="w-3.5 h-3.5" />}
|
||||
</button>
|
||||
|
||||
{/* Close Button */}
|
||||
<button
|
||||
onClick={() => setIsOpen(false)}
|
||||
className="p-1.5 rounded-lg text-slate-400 hover:text-white hover:bg-slate-800 transition-colors"
|
||||
>
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Past Session History Drawer Side Panel */}
|
||||
{showHistory && !isMinimized && (
|
||||
<div className="bg-slate-900 border-b border-slate-800 p-3 max-h-48 overflow-y-auto space-y-2 shrink-0">
|
||||
<div className="flex items-center justify-between text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-1">
|
||||
<span>Saved Past Sessions</span>
|
||||
<span>{pastSessions.length} sessions</span>
|
||||
</div>
|
||||
{loadingHistory ? (
|
||||
<div className="text-center p-3 text-xs text-slate-400 italic">
|
||||
Loading chat history...
|
||||
</div>
|
||||
) : pastSessions.length === 0 ? (
|
||||
<div className="text-center p-3 text-xs text-slate-500 italic">
|
||||
No past sessions found. Start a conversation!
|
||||
</div>
|
||||
) : (
|
||||
pastSessions.map((sess) => {
|
||||
const firstUserMsg = sess.messages?.find(m => m.sender === 'USER')?.content || 'Chat Session';
|
||||
const isActive = sess.id === sessionId;
|
||||
return (
|
||||
<div
|
||||
key={sess.id}
|
||||
onClick={() => 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'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<MessageSquare className="w-3.5 h-3.5 text-emerald-400 shrink-0" />
|
||||
<span className="truncate text-[11px] font-medium">{firstUserMsg}</span>
|
||||
</div>
|
||||
<span className="text-[9px] font-mono text-slate-500 shrink-0 ml-2">
|
||||
{new Date(sess.updatedAt || sess.createdAt).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content Body (Visible when not minimized) */}
|
||||
{!isMinimized && (
|
||||
<>
|
||||
{/* Messages Stream */}
|
||||
<div className="flex-1 p-4 overflow-y-auto space-y-4 text-xs">
|
||||
{messages.map((msg, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={`flex gap-2.5 ${msg.sender === 'USER' ? 'flex-row-reverse' : 'flex-row'}`}
|
||||
>
|
||||
<div className={`p-1.5 rounded-xl shrink-0 h-fit ${
|
||||
msg.sender === 'USER'
|
||||
? 'bg-emerald-600 text-white'
|
||||
: 'bg-slate-900 text-emerald-400 border border-slate-700'
|
||||
}`}>
|
||||
{msg.sender === 'USER' ? <UserIcon className="w-3.5 h-3.5" /> : <Bot className="w-3.5 h-3.5" />}
|
||||
</div>
|
||||
|
||||
<div className={`space-y-2 max-w-[85%] ${msg.sender === 'USER' ? 'text-right' : 'text-left'}`}>
|
||||
<div className={`p-3.5 rounded-2xl leading-relaxed ${
|
||||
msg.sender === 'USER'
|
||||
? 'bg-emerald-600 text-white font-medium rounded-tr-none whitespace-pre-wrap'
|
||||
: 'bg-slate-900 text-slate-100 border border-slate-800 rounded-tl-none shadow-md'
|
||||
}`}>
|
||||
{msg.sender === 'USER' ? (
|
||||
msg.content
|
||||
) : (
|
||||
<MarkdownViewer markdown={msg.content} variant="dark" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Citation Cards with Direct Preview & Location Redirection */}
|
||||
{msg.citations && msg.citations.length > 0 && (
|
||||
<div className="space-y-1.5 pt-1">
|
||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider text-left">
|
||||
Verified Knowledge Sources ({msg.citations.length}):
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
{msg.citations.map((cite) => (
|
||||
<div
|
||||
key={cite.assetId}
|
||||
className="group p-2.5 rounded-xl bg-slate-900/90 border border-slate-800 hover:border-emerald-500/50 transition-all text-left flex flex-col gap-2"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
<FileText className="w-3.5 h-3.5 text-emerald-400 shrink-0 mt-0.5" />
|
||||
<div className="min-w-0">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="font-bold text-[11px] text-white group-hover:text-emerald-300 truncate">
|
||||
{cite.title}
|
||||
</span>
|
||||
{cite.isRecommended && (
|
||||
<span className="px-1.5 py-0.2 rounded bg-amber-500/20 text-amber-300 border border-amber-500/30 text-[8px] font-extrabold uppercase shrink-0">
|
||||
⭐ Recommended
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-[9px] text-slate-400 block font-mono">
|
||||
{cite.location}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action Buttons: Preview & Go to Location */}
|
||||
<div className="flex items-center gap-2 pt-1 border-t border-slate-800">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openAssetPreview(cite)}
|
||||
className="flex-1 py-1 px-2 bg-slate-800 hover:bg-slate-700 text-slate-200 rounded-lg text-[10px] font-bold flex items-center justify-center gap-1 transition-colors cursor-pointer"
|
||||
>
|
||||
<Eye className="w-3 h-3 text-slate-400" />
|
||||
<span>Quick Preview</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleGoToAsset(cite)}
|
||||
className="flex-1 py-1 px-2 bg-emerald-500/20 hover:bg-emerald-500/30 text-emerald-300 border border-emerald-500/40 rounded-lg text-[10px] font-bold flex items-center justify-center gap-1 transition-colors cursor-pointer"
|
||||
>
|
||||
<Compass className="w-3 h-3 text-emerald-400" />
|
||||
<span>Go to Location</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{loading && (
|
||||
<div className="flex items-center gap-2.5 text-slate-400 text-xs italic p-2">
|
||||
<RefreshCw className="w-3.5 h-3.5 animate-spin text-emerald-400" />
|
||||
Analyzing catalog knowledge with DeepSeek...
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
|
||||
{/* Quick Suggestion Chips */}
|
||||
<div className="px-3 py-1.5 bg-slate-900/60 border-t border-slate-800 flex gap-2 overflow-x-auto shrink-0 scrollbar-none">
|
||||
<button
|
||||
onClick={() => handleSend("Is there any NDA Agreement available here?")}
|
||||
className="px-2.5 py-1 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-full text-[10px] font-semibold whitespace-nowrap transition-colors flex items-center gap-1 cursor-pointer"
|
||||
>
|
||||
📄 Check NDA Agreements
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSend("Tell me about our AI and Cybersecurity offerings")}
|
||||
className="px-2.5 py-1 bg-slate-800 hover:bg-slate-700 text-slate-300 rounded-full text-[10px] font-semibold whitespace-nowrap transition-colors flex items-center gap-1 cursor-pointer"
|
||||
>
|
||||
🔍 Cybersecurity Assets
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSend("How to request document downloads or change themes?")}
|
||||
className="px-2.5 py-1 bg-amber-500/10 hover:bg-amber-500/20 text-amber-300 border border-amber-500/30 rounded-full text-[10px] font-semibold whitespace-nowrap transition-colors flex items-center gap-1 cursor-pointer"
|
||||
>
|
||||
⭐ Portal Guide & Help
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Input Controls */}
|
||||
<div className="p-3 bg-slate-900 border-t border-slate-800 flex gap-2 items-center shrink-0">
|
||||
<input
|
||||
type="text"
|
||||
value={prompt}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<button
|
||||
onClick={() => handleSend()}
|
||||
disabled={loading || !prompt.trim()}
|
||||
className="p-2 bg-emerald-500 hover:bg-emerald-400 disabled:opacity-40 text-slate-950 rounded-xl transition-all font-bold cursor-pointer disabled:cursor-not-allowed"
|
||||
>
|
||||
<Send className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Asset Preview Modal Triggered from Citation Click */}
|
||||
{activePreviewAsset && (
|
||||
<AssetViewerModal
|
||||
asset={activePreviewAsset}
|
||||
isOpen={!!activePreviewAsset}
|
||||
user={user}
|
||||
onDownload={() => {}}
|
||||
onClose={() => setActivePreviewAsset(null)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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 <strong key={idx} className="font-extrabold text-ink-900">{part.text}</strong>;
|
||||
case "italic":
|
||||
return <em key={idx} className="italic text-ink-800">{part.text}</em>;
|
||||
case "code":
|
||||
return <code key={idx} className="bg-ink-100 border border-ink-200 rounded px-1.5 py-0.5 text-xs font-mono text-emerald-700">{part.text}</code>;
|
||||
case "link":
|
||||
return (
|
||||
<a
|
||||
key={idx}
|
||||
href={part.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-primary-600 hover:text-primary-800 font-semibold underline break-all inline-flex items-center gap-0.5"
|
||||
>
|
||||
{part.text}
|
||||
</a>
|
||||
);
|
||||
default:
|
||||
return <span key={idx}>{part.text}</span>;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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 (
|
||||
<Component key={key} className={listClass}>
|
||||
{block.items?.map((item, idx) => (
|
||||
<li key={idx}>{renderInlineText(item)}</li>
|
||||
))}
|
||||
</Component>
|
||||
);
|
||||
};
|
||||
|
||||
const renderHeadingBlock = (block: MarkdownBlock, key: string): React.ReactNode => {
|
||||
const level = block.type.slice(1);
|
||||
const classes: Record<string, string> = {
|
||||
"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 (
|
||||
<Component key={key} className={classes[level] || ""}>
|
||||
{renderInlineText(block.content)}
|
||||
</Component>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<blockquote key={key} className="border-l-4 border-primary-500 pl-4 py-1 italic bg-primary-50/20 text-ink-700 my-3 rounded-r-lg">
|
||||
{renderInlineText(block.content)}
|
||||
</blockquote>
|
||||
);
|
||||
case "ul":
|
||||
case "ol":
|
||||
return renderListBlock(block, key);
|
||||
case "code":
|
||||
return (
|
||||
<div key={key} className="my-4 rounded-xl border border-ink-200 bg-ink-900 text-ink-100 p-4 overflow-x-auto shadow-inner relative group select-text">
|
||||
{block.language && (
|
||||
<div className="absolute right-3 top-3 text-[10px] uppercase font-bold text-ink-400 select-none">
|
||||
{block.language}
|
||||
</div>
|
||||
)}
|
||||
<pre className="font-mono text-xs sm:text-sm leading-relaxed overflow-x-auto">
|
||||
{block.content}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
case "hr":
|
||||
return <hr key={key} className="border-t border-ink-200 my-6" />;
|
||||
default:
|
||||
return <p key={key} className="text-sm text-ink-800 leading-relaxed my-2">{renderInlineText(block.content)}</p>;
|
||||
return tokens.map((part, idx) => {
|
||||
switch (part.type) {
|
||||
case "bold":
|
||||
return <strong key={idx} className={isDark ? "font-extrabold text-white" : "font-extrabold text-ink-900"}>{part.text}</strong>;
|
||||
case "italic":
|
||||
return <em key={idx} className={isDark ? "italic text-slate-200" : "italic text-ink-800"}>{part.text}</em>;
|
||||
case "code":
|
||||
return (
|
||||
<code key={idx} className={isDark ? "bg-slate-900 border border-slate-700 text-emerald-300 rounded px-1.5 py-0.5 text-xs font-mono" : "bg-ink-100 border border-ink-200 rounded px-1.5 py-0.5 text-xs font-mono text-emerald-700"}>
|
||||
{part.text}
|
||||
</code>
|
||||
);
|
||||
case "link":
|
||||
return (
|
||||
<a
|
||||
key={idx}
|
||||
href={part.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className={isDark ? "text-emerald-400 hover:text-emerald-300 font-semibold underline break-all inline-flex items-center gap-0.5" : "text-primary-600 hover:text-primary-800 font-semibold underline break-all inline-flex items-center gap-0.5"}
|
||||
>
|
||||
{part.text}
|
||||
</a>
|
||||
);
|
||||
default:
|
||||
return <span key={idx}>{part.text}</span>;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
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 (
|
||||
<Component key={key} className={listClass}>
|
||||
{block.items?.map((item, idx) => (
|
||||
<li key={idx}>{renderInlineText(item, isDark)}</li>
|
||||
))}
|
||||
</Component>
|
||||
);
|
||||
};
|
||||
|
||||
const renderHeadingBlock = (block: MarkdownBlock, key: string, isDark: boolean): React.ReactNode => {
|
||||
const level = block.type.slice(1);
|
||||
const classes: Record<string, string> = {
|
||||
"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 (
|
||||
<Component key={key} className={classes[level] || ""}>
|
||||
{renderInlineText(block.content, isDark)}
|
||||
</Component>
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<blockquote key={key} className={`border-l-4 border-emerald-500 pl-3 py-1 italic ${isDark ? 'bg-slate-900/60 text-slate-200' : 'bg-primary-50/20 text-ink-700'} my-2 rounded-r-lg`}>
|
||||
{renderInlineText(block.content, isDark)}
|
||||
</blockquote>
|
||||
);
|
||||
case "ul":
|
||||
case "ol":
|
||||
return renderListBlock(block, key, isDark);
|
||||
case "code":
|
||||
return (
|
||||
<div key={key} className="my-3 rounded-xl border border-slate-800 bg-slate-950 text-slate-100 p-3 overflow-x-auto shadow-inner relative group select-text">
|
||||
{block.language && (
|
||||
<div className="absolute right-3 top-2 text-[9px] uppercase font-bold text-slate-400 select-none">
|
||||
{block.language}
|
||||
</div>
|
||||
)}
|
||||
<pre className="font-mono text-xs leading-relaxed overflow-x-auto">
|
||||
{block.content}
|
||||
</pre>
|
||||
</div>
|
||||
);
|
||||
case "hr":
|
||||
return <hr key={key} className={`my-4 ${isDark ? 'border-slate-800' : 'border-ink-200'}`} />;
|
||||
default:
|
||||
return (
|
||||
<p key={key} className={`text-xs ${isDark ? 'text-slate-100' : 'text-ink-800'} leading-relaxed my-1.5`}>
|
||||
{renderInlineText(block.content, isDark)}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export interface MarkdownViewerProps {
|
||||
markdown: string;
|
||||
markdown: string;
|
||||
variant?: 'light' | 'dark' | 'auto';
|
||||
}
|
||||
|
||||
export const MarkdownViewer: React.FC<MarkdownViewerProps> = ({ markdown }) => {
|
||||
const blocks = parseMarkdown(markdown);
|
||||
return (
|
||||
<div className="w-full text-left select-text font-sans leading-relaxed text-ink-800">
|
||||
{blocks.map((block, idx) => renderBlock(block, idx))}
|
||||
</div>
|
||||
);
|
||||
export const MarkdownViewer: React.FC<MarkdownViewerProps> = ({ markdown, variant = 'auto' }) => {
|
||||
const blocks = parseMarkdown(markdown);
|
||||
const isDark = variant === 'dark';
|
||||
return (
|
||||
<div className={`w-full text-left select-text font-sans leading-relaxed ${isDark ? 'text-slate-100' : 'text-ink-800'}`}>
|
||||
{blocks.map((block, idx) => renderBlock(block, idx, isDark))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarkdownViewer;
|
||||
|
||||
@ -145,6 +145,7 @@ export const AssetCard: React.FC<AssetCardProps> = ({
|
||||
|
||||
return (
|
||||
<motion.div
|
||||
id={`asset-card-${asset.id}`}
|
||||
layout
|
||||
transition={{ type: "spring", stiffness: 320, damping: 28 }}
|
||||
onClick={(e) => {
|
||||
|
||||
@ -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<AssetViewerModalProps> = ({
|
||||
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<AssetViewerModalProps> = ({
|
||||
const [isLoadingDoc, setIsLoadingDoc] = useState(false);
|
||||
const [docLoadError, setDocLoadError] = useState(false);
|
||||
const wordContainerRef = React.useRef<HTMLDivElement>(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<AssetViewerModalProps> = ({
|
||||
}
|
||||
}, [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<AssetViewerModalProps> = ({
|
||||
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 (
|
||||
<Modal
|
||||
isOpen={isOpen && !!asset}
|
||||
@ -261,7 +309,7 @@ export const AssetViewerModal: React.FC<AssetViewerModalProps> = ({
|
||||
<div className="text-left">
|
||||
<span className="text-base font-bold text-ink-900 block font-sans">{asset?.title}</span>
|
||||
<span className="text-xs text-ink-500 font-sans block mt-0.5 font-normal">
|
||||
{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}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
@ -312,7 +360,19 @@ export const AssetViewerModal: React.FC<AssetViewerModalProps> = ({
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{asset && asset.type !== 'url' && (user?.role === 'ADMIN' || asset.isDownloadable || asset.downloadRequests?.[0]?.status === 'APPROVED') && (
|
||||
{asset && (
|
||||
<Button
|
||||
onClick={handleLocateInCatalog}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="flex items-center gap-2 text-emerald-600 dark:text-emerald-400 border-emerald-500/30 hover:bg-emerald-500/10"
|
||||
>
|
||||
<Compass className="w-4 h-4" />
|
||||
<span>Locate in Portal Catalog</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{asset && asset.type !== 'url' && asset.type !== 'case_study' && (user?.role === 'ADMIN' || asset.isDownloadable || asset.downloadRequests?.[0]?.status === 'APPROVED') && (
|
||||
<Button
|
||||
onClick={() => onDownload(asset)}
|
||||
variant="primary"
|
||||
@ -495,6 +555,61 @@ export const AssetViewerModal: React.FC<AssetViewerModalProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : asset.type === 'case_study' || asset.type.includes('video') || asset.type === 'showcase' ? (
|
||||
<div className="w-full h-full flex flex-col min-h-0 bg-ink-0">
|
||||
<div className="bg-ink-100 px-4 py-2 border-b border-ink-200 flex justify-between items-center text-xs text-ink-600 font-bold select-none shrink-0">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Sparkles className="w-4 h-4 text-emerald-600" />
|
||||
<span className="font-sans">Executive Case Study & Video Showcase</span>
|
||||
</span>
|
||||
<span className="text-[10px] text-ink-400 font-mono font-normal truncate max-w-xs">{asset.title}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto p-6 md:p-8 flex flex-col items-center justify-start gap-6 bg-ink-50">
|
||||
{asset.url && (asset.url.includes('youtube.com') || asset.url.includes('youtu.be')) ? (
|
||||
<div className="w-full max-w-3xl aspect-video rounded-2xl overflow-hidden shadow-2xl bg-black border border-ink-300 shrink-0">
|
||||
<iframe
|
||||
src={`https://www.youtube-nocookie.com/embed/${extractYouTubeVideoId(asset.url)}?autoplay=1&rel=0`}
|
||||
className="w-full h-full border-0"
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
allowFullScreen
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full max-w-3xl p-6 bg-slate-900 text-white rounded-2xl shadow-xl border border-slate-800 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="px-2 py-0.5 rounded text-[10px] font-extrabold uppercase bg-emerald-500/20 text-emerald-400 border border-emerald-500/30">
|
||||
Verified Showcase Reel
|
||||
</span>
|
||||
</div>
|
||||
<h2 className="text-lg font-bold">{asset.title}</h2>
|
||||
{asset.description && <p className="text-xs text-slate-300 leading-relaxed whitespace-pre-wrap">{asset.description}</p>}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="w-full max-w-3xl bg-ink-0 border border-ink-200 rounded-2xl p-6 shadow-md space-y-4 text-left">
|
||||
<h3 className="text-base font-extrabold text-ink-900">{asset.title}</h3>
|
||||
{asset.problemStatement && (
|
||||
<div className="space-y-1">
|
||||
<span className="text-xs font-bold text-red-600 uppercase tracking-wider">Problem Statement</span>
|
||||
<p className="text-xs text-ink-700 leading-relaxed">{asset.problemStatement}</p>
|
||||
</div>
|
||||
)}
|
||||
{asset.solution && (
|
||||
<div className="space-y-1 pt-2 border-t border-ink-150">
|
||||
<span className="text-xs font-bold text-emerald-600 uppercase tracking-wider">Implemented Technical Solution</span>
|
||||
<p className="text-xs text-ink-700 leading-relaxed">{asset.solution}</p>
|
||||
</div>
|
||||
)}
|
||||
{asset.description && !asset.problemStatement && (
|
||||
<div className="space-y-1 pt-2 border-t border-ink-150">
|
||||
<span className="text-xs font-bold text-ink-600 uppercase tracking-wider">Executive Overview</span>
|
||||
<p className="text-xs text-ink-700 leading-relaxed whitespace-pre-wrap">{asset.description}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : asset.type.includes('pdf') ? (
|
||||
<div className="w-full h-full flex flex-col min-h-0">
|
||||
<div className="bg-ink-100 px-4 py-2 border-b border-ink-200 flex items-center text-xs text-ink-600 font-bold select-none">
|
||||
@ -691,6 +806,100 @@ export const AssetViewerModal: React.FC<AssetViewerModalProps> = ({
|
||||
className="max-w-full max-h-full object-contain rounded-xl shadow-sm border border-ink-100"
|
||||
/>
|
||||
</div>
|
||||
) : (asset.type === 'case_study' || asset.problemStatement || asset.solution) ? (
|
||||
<div className="w-full h-full flex flex-col min-h-0 bg-ink-0 overflow-y-auto p-6 text-left select-text">
|
||||
<div className="max-w-4xl mx-auto w-full space-y-6">
|
||||
{asset.thumbnailUrl && (
|
||||
<div className="w-full h-48 rounded-2xl overflow-hidden border border-ink-200 shadow-sm relative bg-ink-100">
|
||||
<img src={asset.thumbnailUrl} alt={asset.title} className="w-full h-full object-cover" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="border-b border-ink-200 pb-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span className="px-2.5 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider bg-emerald-500/10 text-emerald-600 border border-emerald-500/20">
|
||||
Executive Case Study Showcase
|
||||
</span>
|
||||
{asset.type && (
|
||||
<span className="px-2 py-0.5 rounded text-[10px] font-semibold bg-ink-100 text-ink-700 capitalize">
|
||||
{asset.type}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<h1 className="text-xl sm:text-2xl font-extrabold text-ink-900 leading-tight">{asset.title}</h1>
|
||||
{asset.description && <p className="text-xs text-ink-600 mt-2 leading-relaxed">{asset.description}</p>}
|
||||
</div>
|
||||
|
||||
{/* Problem Statement Block */}
|
||||
{asset.problemStatement && (
|
||||
<div className="p-4 rounded-xl bg-amber-500/5 border border-amber-500/20 space-y-2">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-amber-700 dark:text-amber-400 flex items-center gap-2">
|
||||
<Sparkles className="w-4 h-4 text-amber-500" />
|
||||
Problem Statement & Challenge
|
||||
</h3>
|
||||
<div className="text-xs text-ink-800 dark:text-slate-200 leading-relaxed font-sans">
|
||||
<MarkdownViewer markdown={asset.problemStatement} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Proposed Solution Block */}
|
||||
{asset.solution && (
|
||||
<div className="p-4 rounded-xl bg-emerald-500/5 border border-emerald-500/20 space-y-2">
|
||||
<h3 className="text-xs font-bold uppercase tracking-wider text-emerald-700 dark:text-emerald-400 flex items-center gap-2">
|
||||
<Sparkles className="w-4 h-4 text-emerald-500" />
|
||||
Implemented Technical Solution
|
||||
</h3>
|
||||
<div className="text-xs text-ink-800 dark:text-slate-200 leading-relaxed font-sans">
|
||||
<MarkdownViewer markdown={asset.solution} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{asset.url && asset.url.startsWith('http') && (
|
||||
<div className="pt-2 flex justify-end">
|
||||
<a
|
||||
href={asset.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="px-4 py-2 bg-ink-900 text-ink-0 hover:bg-ink-800 rounded-xl text-xs font-bold flex items-center gap-2 transition-colors"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
Visit Showcase Webpage
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : asset.type === 'url' ? (
|
||||
<div className="w-full h-full flex flex-col min-h-0 bg-ink-0 overflow-y-auto p-6 text-left select-text">
|
||||
<div className="max-w-3xl mx-auto w-full space-y-6 text-center flex flex-col items-center justify-center h-full">
|
||||
<div className="w-16 h-16 rounded-2xl bg-blue-500/10 border border-blue-500/20 flex items-center justify-center text-blue-600">
|
||||
<Globe className="w-8 h-8" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="px-2.5 py-1 rounded-full text-[10px] font-bold uppercase tracking-wider bg-blue-500/10 text-blue-600 border border-blue-500/20">
|
||||
External Web Resource
|
||||
</span>
|
||||
<h2 className="text-xl font-extrabold text-ink-900 mt-3">{asset.title}</h2>
|
||||
{asset.description && <p className="text-xs text-ink-600 mt-2 max-w-lg mx-auto">{asset.description}</p>}
|
||||
</div>
|
||||
|
||||
<div className="p-4 bg-ink-50 border border-ink-200 rounded-xl w-full max-w-md text-left">
|
||||
<p className="text-[10px] font-mono text-ink-500 truncate">{asset.url}</p>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href={asset.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="px-6 py-3 bg-emerald-600 hover:bg-emerald-700 text-white font-bold rounded-xl text-xs flex items-center gap-2 shadow-md transition-all cursor-pointer"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
Launch Web Document
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center p-8 flex flex-col justify-center items-center">
|
||||
<File className="w-12 h-12 text-ink-300 mb-3" />
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { X, UploadCloud, Eye, FileText, File } from 'lucide-react';
|
||||
import { X, UploadCloud, Eye, FileText, File, Sparkles } from 'lucide-react';
|
||||
import { uploadAsset, scrapeCaseStudy, getTaxonomyMeta } from '../../../services/assets-api';
|
||||
import type { TaxonomyMeta } from '../../../types/assets';
|
||||
import Modal from '../../../components/ui/Modal';
|
||||
@ -42,6 +42,7 @@ export const UploadAssetModal: React.FC<UploadAssetModalProps> = ({
|
||||
const [uploadTags, setUploadTags] = useState('');
|
||||
const [uploadGithubUrl, setUploadGithubUrl] = useState('');
|
||||
const [uploadIsDownloadable, setUploadIsDownloadable] = useState(true);
|
||||
const [includeInKnowledgeBase, setIncludeInKnowledgeBase] = useState(true);
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@ -182,6 +183,7 @@ export const UploadAssetModal: React.FC<UploadAssetModalProps> = ({
|
||||
formData.append('problemStatement', problemStatement);
|
||||
formData.append('solution', solution);
|
||||
}
|
||||
formData.append('includeInKnowledgeBase', String(includeInKnowledgeBase));
|
||||
|
||||
try {
|
||||
await uploadAsset(formData);
|
||||
@ -646,6 +648,25 @@ export const UploadAssetModal: React.FC<UploadAssetModalProps> = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-start gap-3 p-3 bg-emerald-500/5 dark:bg-emerald-500/10 border border-emerald-500/20 rounded-xl">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="includeInKnowledgeBase"
|
||||
checked={includeInKnowledgeBase}
|
||||
onChange={(e) => setIncludeInKnowledgeBase(e.target.checked)}
|
||||
className="w-4 h-4 rounded border-emerald-300 text-emerald-600 focus:ring-emerald-500 cursor-pointer mt-0.5"
|
||||
/>
|
||||
<div>
|
||||
<label htmlFor="includeInKnowledgeBase" className="text-xs font-bold text-ink-900 dark:text-emerald-300 cursor-pointer flex items-center gap-1.5">
|
||||
<Sparkles className="w-3.5 h-3.5 text-emerald-500" />
|
||||
Include in AI Advisor Knowledge Base (RAG Search)
|
||||
</label>
|
||||
<span className="text-[10px] text-ink-500 dark:text-slate-400 block mt-0.5">
|
||||
If checked, DeepSeek AI Advisor indexes this content to answer partner questions with OKF standard citations.
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="text-xs font-semibold text-ink-500 mb-1.5 block">Tags (comma-separated)</label>
|
||||
<input
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { useState, useEffect } from "react";
|
||||
import { useLocation } from "react-router-dom";
|
||||
import { motion } from "framer-motion";
|
||||
import type { Variants } from "framer-motion";
|
||||
import {
|
||||
@ -58,6 +59,7 @@ const itemVariants: Variants = {
|
||||
};
|
||||
|
||||
export const AssetsPage = () => {
|
||||
const location = useLocation();
|
||||
const { success, error } = useToast();
|
||||
const user = useAuthStore((state) => state.user);
|
||||
const [assets, setAssets] = useState<Asset[]>([]);
|
||||
@ -99,6 +101,23 @@ export const AssetsPage = () => {
|
||||
);
|
||||
})();
|
||||
|
||||
useEffect(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const targetId = urlParams.get('highlight') || location.state?.highlightAssetId;
|
||||
if (targetId && !loading) {
|
||||
setTimeout(() => {
|
||||
const el = document.getElementById(`asset-card-${targetId}`);
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: "smooth", block: "center" });
|
||||
el.classList.add("ring-4", "ring-emerald-500", "scale-[1.03]", "shadow-2xl", "transition-all", "duration-500");
|
||||
setTimeout(() => {
|
||||
el.classList.remove("ring-4", "ring-emerald-500", "scale-[1.03]", "shadow-2xl");
|
||||
}, 5000);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}, [location.state, location.search, loading]);
|
||||
|
||||
// Modal Control States
|
||||
const [isUploadOpen, setIsUploadOpen] = useState(false);
|
||||
const [isEditOpen, setIsEditOpen] = useState(false);
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { Shield, FileText, CheckCircle, Clock, Download, ExternalLink, RefreshCw } from 'lucide-react';
|
||||
import { useAuthStore } from '../hooks/use-auth';
|
||||
import { useMyAcceptancesQuery } from '../hooks/use-legal-query';
|
||||
@ -8,10 +9,28 @@ import { PageLayout } from '../components/layout/PageLayout';
|
||||
import Modal from '../components/ui/Modal';
|
||||
|
||||
export const ClientAgreementsPage: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const user = useAuthStore((state) => state.user);
|
||||
const { data: acceptances, isLoading, refetch, isFetching } = useMyAcceptancesQuery();
|
||||
const [selectedDoc, setSelectedDoc] = useState<'NDA' | 'MSA' | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const targetId = urlParams.get('highlight') || location.state?.highlightAssetId;
|
||||
if (targetId && !isLoading) {
|
||||
setTimeout(() => {
|
||||
const el = document.getElementById(`asset-card-${targetId}`) || document.getElementById('asset-card-nda');
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
el.classList.add('ring-4', 'ring-emerald-500', 'scale-[1.03]', 'shadow-2xl', 'transition-all', 'duration-500');
|
||||
setTimeout(() => {
|
||||
el.classList.remove('ring-4', 'ring-emerald-500', 'scale-[1.03]', 'shadow-2xl');
|
||||
}, 5000);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}, [location.state, location.search, isLoading]);
|
||||
|
||||
const ndaAcceptance = acceptances?.find(a => a.document.type === 'NDA');
|
||||
const msaAcceptance = acceptances?.find(a => a.document.type === 'MSA');
|
||||
|
||||
@ -61,7 +80,7 @@ export const ClientAgreementsPage: React.FC = () => {
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* NDA Card */}
|
||||
<div className="bg-ink-0 border border-ink-200 rounded-2xl p-5 shadow-sm flex flex-col justify-between hover:border-ink-400 transition-all duration-300">
|
||||
<div id={`asset-card-${ndaAcceptance?.document?.id || 'nda'}`} className="bg-ink-0 border border-ink-200 rounded-2xl p-5 shadow-sm flex flex-col justify-between hover:border-ink-400 transition-all duration-500">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="w-10 h-10 rounded-xl bg-ink-900 text-ink-0 flex items-center justify-center">
|
||||
@ -136,7 +155,7 @@ export const ClientAgreementsPage: React.FC = () => {
|
||||
</div>
|
||||
|
||||
{/* MSA Card */}
|
||||
<div className="bg-ink-0 border border-ink-200 rounded-2xl p-5 shadow-sm flex flex-col justify-between hover:border-ink-400 transition-all duration-300">
|
||||
<div id={`asset-card-${msaAcceptance?.document?.id || 'msa'}`} className="bg-ink-0 border border-ink-200 rounded-2xl p-5 shadow-sm flex flex-col justify-between hover:border-ink-400 transition-all duration-500">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="w-10 h-10 rounded-xl bg-ink-900 text-ink-0 flex items-center justify-center">
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
Code,
|
||||
@ -24,10 +25,28 @@ const iconMap: Record<string, any> = {
|
||||
};
|
||||
|
||||
export const EcosystemPage: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const [offerings, setOfferings] = useState<EcosystemOffering[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [filter, setFilter] = useState<'ALL' | 'PRODUCT' | 'SERVICE'>('ALL');
|
||||
|
||||
useEffect(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const targetId = urlParams.get('highlight') || location.state?.highlightAssetId;
|
||||
if (targetId && !loading) {
|
||||
setTimeout(() => {
|
||||
const el = document.getElementById(`asset-card-${targetId}`);
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
el.classList.add('ring-4', 'ring-emerald-500', 'scale-[1.03]', 'shadow-2xl', 'transition-all', 'duration-500');
|
||||
setTimeout(() => {
|
||||
el.classList.remove('ring-4', 'ring-emerald-500', 'scale-[1.03]', 'shadow-2xl');
|
||||
}, 5000);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}, [location.state, location.search, loading]);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
@ -107,6 +126,7 @@ export const EcosystemPage: React.FC = () => {
|
||||
return (
|
||||
<div
|
||||
key={offering.id}
|
||||
id={`asset-card-${offering.id}`}
|
||||
className="group flex flex-col justify-between bg-ink-0 border border-ink-200 hover:border-ink-450 rounded-3xl p-6 md:p-8 hover:shadow-xl transition-all duration-500"
|
||||
>
|
||||
<div>
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import {
|
||||
Play,
|
||||
@ -150,10 +151,28 @@ const VideoDescription: React.FC<VideoDescriptionProps> = ({ text, isExpanded, o
|
||||
};
|
||||
|
||||
export const ShowcasePage: React.FC = () => {
|
||||
const location = useLocation();
|
||||
const [items, setItems] = useState<ContentShowcase[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [playingVideoId, setPlayingVideoId] = useState<string | null>(null);
|
||||
const [expandedItemId, setExpandedItemId] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const targetId = urlParams.get('highlight') || location.state?.highlightAssetId;
|
||||
if (targetId && !loading) {
|
||||
setTimeout(() => {
|
||||
const el = document.getElementById(`asset-card-${targetId}`);
|
||||
if (el) {
|
||||
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
el.classList.add('ring-4', 'ring-emerald-500', 'scale-[1.03]', 'shadow-2xl', 'transition-all', 'duration-500');
|
||||
setTimeout(() => {
|
||||
el.classList.remove('ring-4', 'ring-emerald-500', 'scale-[1.03]', 'shadow-2xl');
|
||||
}, 5000);
|
||||
}
|
||||
}, 300);
|
||||
}
|
||||
}, [location.state, location.search, loading]);
|
||||
|
||||
// Resizable Lightbox state: compact | theater | cinema
|
||||
const [lightboxSize, setLightboxSize] = useState<'compact' | 'theater' | 'cinema'>('compact');
|
||||
@ -219,7 +238,7 @@ export const ShowcasePage: React.FC = () => {
|
||||
const isExpanded = expandedItemId === item.id;
|
||||
|
||||
return (
|
||||
<div key={item.id} className="relative h-[410px] w-full flex flex-col">
|
||||
<div key={item.id} id={`asset-card-${item.id}`} className="relative h-[410px] w-full flex flex-col">
|
||||
<motion.div
|
||||
layout
|
||||
transition={{ type: "spring", stiffness: 320, damping: 28 }}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user