Add HTML & Markdown preview in dashboard and revise pages
This commit is contained in:
575
package-lock.json
generated
575
package-lock.json
generated
@@ -14,6 +14,7 @@
|
|||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.2",
|
||||||
"connect-flash": "^0.1.1",
|
"connect-flash": "^0.1.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"dompurify": "^3.2.6",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"exceljs": "^4.4.0",
|
"exceljs": "^4.4.0",
|
||||||
@@ -21,7 +22,9 @@
|
|||||||
"express-session": "^1.18.0",
|
"express-session": "^1.18.0",
|
||||||
"form-data": "^4.0.3",
|
"form-data": "^4.0.3",
|
||||||
"fs-extra": "^11.2.0",
|
"fs-extra": "^11.2.0",
|
||||||
|
"jsdom": "^26.1.0",
|
||||||
"mammoth": "^1.9.1",
|
"mammoth": "^1.9.1",
|
||||||
|
"marked": "^16.0.0",
|
||||||
"multer": "^2.0.0",
|
"multer": "^2.0.0",
|
||||||
"pdf-parse": "^1.1.1",
|
"pdf-parse": "^1.1.1",
|
||||||
"uuid": "^10.0.0"
|
"uuid": "^10.0.0"
|
||||||
@@ -30,6 +33,129 @@
|
|||||||
"nodemon": "^3.0.1"
|
"nodemon": "^3.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@asamuzakjp/css-color": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@csstools/css-calc": "^2.1.3",
|
||||||
|
"@csstools/css-color-parser": "^3.0.9",
|
||||||
|
"@csstools/css-parser-algorithms": "^3.0.4",
|
||||||
|
"@csstools/css-tokenizer": "^3.0.3",
|
||||||
|
"lru-cache": "^10.4.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/color-helpers": {
|
||||||
|
"version": "5.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz",
|
||||||
|
"integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT-0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-calc": {
|
||||||
|
"version": "2.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
|
||||||
|
"integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||||
|
"@csstools/css-tokenizer": "^3.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-color-parser": {
|
||||||
|
"version": "3.0.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz",
|
||||||
|
"integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@csstools/color-helpers": "^5.0.2",
|
||||||
|
"@csstools/css-calc": "^2.1.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||||
|
"@csstools/css-tokenizer": "^3.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-parser-algorithms": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@csstools/css-tokenizer": "^3.0.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@csstools/css-tokenizer": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/csstools"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/csstools"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@fast-csv/format": {
|
"node_modules/@fast-csv/format": {
|
||||||
"version": "4.3.5",
|
"version": "4.3.5",
|
||||||
"resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz",
|
||||||
@@ -85,6 +211,13 @@
|
|||||||
"integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==",
|
"integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/trusted-types": {
|
||||||
|
"version": "2.0.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
|
||||||
|
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@xmldom/xmldom": {
|
"node_modules/@xmldom/xmldom": {
|
||||||
"version": "0.8.10",
|
"version": "0.8.10",
|
||||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
|
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
|
||||||
@@ -794,6 +927,66 @@
|
|||||||
"node": ">= 10"
|
"node": ">= 10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cssstyle": {
|
||||||
|
"version": "4.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
|
||||||
|
"integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@asamuzakjp/css-color": "^3.2.0",
|
||||||
|
"rrweb-cssom": "^0.8.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/data-urls": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-mimetype": "^4.0.0",
|
||||||
|
"whatwg-url": "^14.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/data-urls/node_modules/tr46": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"punycode": "^2.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/data-urls/node_modules/webidl-conversions": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/data-urls/node_modules/whatwg-url": {
|
||||||
|
"version": "14.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
|
||||||
|
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tr46": "^5.1.0",
|
||||||
|
"webidl-conversions": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dayjs": {
|
"node_modules/dayjs": {
|
||||||
"version": "1.11.13",
|
"version": "1.11.13",
|
||||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||||
@@ -809,6 +1002,12 @@
|
|||||||
"ms": "2.0.0"
|
"ms": "2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/decimal.js": {
|
||||||
|
"version": "10.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
||||||
|
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/delayed-stream": {
|
"node_modules/delayed-stream": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
@@ -858,6 +1057,15 @@
|
|||||||
"integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==",
|
"integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==",
|
||||||
"license": "BSD-2-Clause"
|
"license": "BSD-2-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/dompurify": {
|
||||||
|
"version": "3.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",
|
||||||
|
"integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",
|
||||||
|
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@types/trusted-types": "^2.0.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dotenv": {
|
"node_modules/dotenv": {
|
||||||
"version": "16.6.1",
|
"version": "16.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||||
@@ -977,6 +1185,18 @@
|
|||||||
"once": "^1.4.0"
|
"once": "^1.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/entities": {
|
||||||
|
"version": "6.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
||||||
|
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.12"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/es-define-property": {
|
"node_modules/es-define-property": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||||
@@ -1541,6 +1761,18 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/html-encoding-sniffer": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-encoding": "^3.1.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/http-errors": {
|
"node_modules/http-errors": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||||
@@ -1557,6 +1789,51 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/http-proxy-agent": {
|
||||||
|
"version": "7.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
||||||
|
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "^7.1.0",
|
||||||
|
"debug": "^4.3.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/http-proxy-agent/node_modules/agent-base": {
|
||||||
|
"version": "7.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||||
|
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/http-proxy-agent/node_modules/debug": {
|
||||||
|
"version": "4.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||||
|
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/http-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/https-proxy-agent": {
|
"node_modules/https-proxy-agent": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||||
@@ -1719,6 +1996,12 @@
|
|||||||
"node": ">=0.12.0"
|
"node": ">=0.12.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/is-potential-custom-element-name": {
|
||||||
|
"version": "1.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
|
||||||
|
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/isarray": {
|
"node_modules/isarray": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||||
@@ -1743,6 +2026,136 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/jsdom": {
|
||||||
|
"version": "26.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
|
||||||
|
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"cssstyle": "^4.2.1",
|
||||||
|
"data-urls": "^5.0.0",
|
||||||
|
"decimal.js": "^10.5.0",
|
||||||
|
"html-encoding-sniffer": "^4.0.0",
|
||||||
|
"http-proxy-agent": "^7.0.2",
|
||||||
|
"https-proxy-agent": "^7.0.6",
|
||||||
|
"is-potential-custom-element-name": "^1.0.1",
|
||||||
|
"nwsapi": "^2.2.16",
|
||||||
|
"parse5": "^7.2.1",
|
||||||
|
"rrweb-cssom": "^0.8.0",
|
||||||
|
"saxes": "^6.0.0",
|
||||||
|
"symbol-tree": "^3.2.4",
|
||||||
|
"tough-cookie": "^5.1.1",
|
||||||
|
"w3c-xmlserializer": "^5.0.0",
|
||||||
|
"webidl-conversions": "^7.0.0",
|
||||||
|
"whatwg-encoding": "^3.1.1",
|
||||||
|
"whatwg-mimetype": "^4.0.0",
|
||||||
|
"whatwg-url": "^14.1.1",
|
||||||
|
"ws": "^8.18.0",
|
||||||
|
"xml-name-validator": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"canvas": "^3.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"canvas": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jsdom/node_modules/agent-base": {
|
||||||
|
"version": "7.1.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||||
|
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jsdom/node_modules/debug": {
|
||||||
|
"version": "4.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||||
|
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.1.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"supports-color": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jsdom/node_modules/https-proxy-agent": {
|
||||||
|
"version": "7.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||||
|
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"agent-base": "^7.1.2",
|
||||||
|
"debug": "4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jsdom/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/jsdom/node_modules/saxes": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"xmlchars": "^2.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=v12.22.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jsdom/node_modules/tr46": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"punycode": "^2.3.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jsdom/node_modules/webidl-conversions": {
|
||||||
|
"version": "7.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||||
|
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/jsdom/node_modules/whatwg-url": {
|
||||||
|
"version": "14.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
|
||||||
|
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tr46": "^5.1.0",
|
||||||
|
"webidl-conversions": "^7.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/jsonfile": {
|
"node_modules/jsonfile": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
|
||||||
@@ -1944,6 +2357,12 @@
|
|||||||
"underscore": "^1.13.1"
|
"underscore": "^1.13.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lru-cache": {
|
||||||
|
"version": "10.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||||
|
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/make-dir": {
|
"node_modules/make-dir": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||||
@@ -1992,6 +2411,18 @@
|
|||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/marked": {
|
||||||
|
"version": "16.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/marked/-/marked-16.0.0.tgz",
|
||||||
|
"integrity": "sha512-MUKMXDjsD/eptB7GPzxo4xcnLS6oo7/RHimUMHEDRhUooPwmN9BEpMl7AEOJv3bmso169wHI2wUF9VQgL7zfmA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"marked": "bin/marked.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@@ -2307,6 +2738,12 @@
|
|||||||
"set-blocking": "^2.0.0"
|
"set-blocking": "^2.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/nwsapi": {
|
||||||
|
"version": "2.2.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz",
|
||||||
|
"integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/object-assign": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
@@ -2370,6 +2807,18 @@
|
|||||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||||
"license": "(MIT AND Zlib)"
|
"license": "(MIT AND Zlib)"
|
||||||
},
|
},
|
||||||
|
"node_modules/parse5": {
|
||||||
|
"version": "7.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
||||||
|
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"entities": "^6.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/parseurl": {
|
"node_modules/parseurl": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||||
@@ -2467,6 +2916,15 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/punycode": {
|
||||||
|
"version": "2.3.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
|
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.13.0",
|
"version": "6.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||||
@@ -2588,6 +3046,12 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rrweb-cssom": {
|
||||||
|
"version": "0.8.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
|
||||||
|
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/safe-buffer": {
|
"node_modules/safe-buffer": {
|
||||||
"version": "5.2.1",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||||
@@ -2871,6 +3335,12 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/symbol-tree": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tar": {
|
"node_modules/tar": {
|
||||||
"version": "6.2.1",
|
"version": "6.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
||||||
@@ -2916,6 +3386,24 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tldts": {
|
||||||
|
"version": "6.1.86",
|
||||||
|
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
|
||||||
|
"integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tldts-core": "^6.1.86"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"tldts": "bin/cli.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/tldts-core": {
|
||||||
|
"version": "6.1.86",
|
||||||
|
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
|
||||||
|
"integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tmp": {
|
"node_modules/tmp": {
|
||||||
"version": "0.2.3",
|
"version": "0.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
|
||||||
@@ -2957,6 +3445,18 @@
|
|||||||
"nodetouch": "bin/nodetouch.js"
|
"nodetouch": "bin/nodetouch.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tough-cookie": {
|
||||||
|
"version": "5.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
|
||||||
|
"integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"tldts": "^6.1.32"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tr46": {
|
"node_modules/tr46": {
|
||||||
"version": "0.0.3",
|
"version": "0.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||||
@@ -3119,12 +3619,57 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/w3c-xmlserializer": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"xml-name-validator": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/webidl-conversions": {
|
"node_modules/webidl-conversions": {
|
||||||
"version": "3.0.1",
|
"version": "3.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||||
"license": "BSD-2-Clause"
|
"license": "BSD-2-Clause"
|
||||||
},
|
},
|
||||||
|
"node_modules/whatwg-encoding": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"iconv-lite": "0.6.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-encoding/node_modules/iconv-lite": {
|
||||||
|
"version": "0.6.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||||
|
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/whatwg-mimetype": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/whatwg-url": {
|
"node_modules/whatwg-url": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||||
@@ -3150,6 +3695,36 @@
|
|||||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/ws": {
|
||||||
|
"version": "8.18.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
|
||||||
|
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"bufferutil": "^4.0.1",
|
||||||
|
"utf-8-validate": ">=5.0.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"bufferutil": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"utf-8-validate": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/xml-name-validator": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/xmlbuilder": {
|
"node_modules/xmlbuilder": {
|
||||||
"version": "10.1.1",
|
"version": "10.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz",
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.2",
|
||||||
"connect-flash": "^0.1.1",
|
"connect-flash": "^0.1.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
|
"dompurify": "^3.2.6",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"exceljs": "^4.4.0",
|
"exceljs": "^4.4.0",
|
||||||
@@ -20,7 +21,9 @@
|
|||||||
"express-session": "^1.18.0",
|
"express-session": "^1.18.0",
|
||||||
"form-data": "^4.0.3",
|
"form-data": "^4.0.3",
|
||||||
"fs-extra": "^11.2.0",
|
"fs-extra": "^11.2.0",
|
||||||
|
"jsdom": "^26.1.0",
|
||||||
"mammoth": "^1.9.1",
|
"mammoth": "^1.9.1",
|
||||||
|
"marked": "^16.0.0",
|
||||||
"multer": "^2.0.0",
|
"multer": "^2.0.0",
|
||||||
"pdf-parse": "^1.1.1",
|
"pdf-parse": "^1.1.1",
|
||||||
"uuid": "^10.0.0"
|
"uuid": "^10.0.0"
|
||||||
|
|||||||
@@ -740,20 +740,231 @@ body {
|
|||||||
border-color: rgba(255,255,255,0.3);
|
border-color: rgba(255,255,255,0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive adjustments for mobile */
|
/* Rendered Markdown Styles */
|
||||||
|
.rendered-markdown {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
max-width: 100%;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rendered-markdown h1,
|
||||||
|
.rendered-markdown h2,
|
||||||
|
.rendered-markdown h3,
|
||||||
|
.rendered-markdown h4,
|
||||||
|
.rendered-markdown h5,
|
||||||
|
.rendered-markdown h6 {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rendered-markdown h1 {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
border-bottom: 2px solid #e1e4e8;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rendered-markdown h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
border-bottom: 1px solid #e1e4e8;
|
||||||
|
padding-bottom: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rendered-markdown h3 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rendered-markdown h4 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rendered-markdown h5,
|
||||||
|
.rendered-markdown h6 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rendered-markdown p {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rendered-markdown strong,
|
||||||
|
.rendered-markdown b {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rendered-markdown em,
|
||||||
|
.rendered-markdown i {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rendered-markdown ul,
|
||||||
|
.rendered-markdown ol {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding-left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rendered-markdown li {
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rendered-markdown li > p {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rendered-markdown ul {
|
||||||
|
list-style-type: disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rendered-markdown ol {
|
||||||
|
list-style-type: decimal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rendered-markdown blockquote {
|
||||||
|
margin: 1rem 0;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-left: 4px solid #dfe2e5;
|
||||||
|
background-color: #f6f8fa;
|
||||||
|
color: #6a737d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rendered-markdown blockquote p:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rendered-markdown pre {
|
||||||
|
background-color: #f6f8fa;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.45;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-family: 'Courier New', Consolas, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rendered-markdown code {
|
||||||
|
background-color: rgba(27, 31, 35, 0.05);
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
font-family: 'Courier New', Consolas, monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rendered-markdown pre code {
|
||||||
|
background-color: transparent;
|
||||||
|
border-radius: 0;
|
||||||
|
font-size: inherit;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
word-break: normal;
|
||||||
|
white-space: pre;
|
||||||
|
word-wrap: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rendered-markdown table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rendered-markdown table th,
|
||||||
|
.rendered-markdown table td {
|
||||||
|
border: 1px solid #dfe2e5;
|
||||||
|
padding: 6px 13px;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rendered-markdown table th {
|
||||||
|
background-color: #f6f8fa;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rendered-markdown table tr:nth-child(2n) {
|
||||||
|
background-color: #f6f8fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rendered-markdown a {
|
||||||
|
color: #0366d6;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rendered-markdown a:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rendered-markdown hr {
|
||||||
|
border: none;
|
||||||
|
border-top: 1px solid #e1e4e8;
|
||||||
|
height: 1px;
|
||||||
|
margin: 1.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rendered-markdown img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 6px;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Display mode toggle styling */
|
||||||
|
#display-mode-toggle .btn-outline-secondary {
|
||||||
|
border-color: #6c757d;
|
||||||
|
color: #6c757d;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#display-mode-toggle .btn-check:checked + .btn-outline-secondary {
|
||||||
|
background-color: #6c757d;
|
||||||
|
border-color: #6c757d;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
#display-mode-toggle .btn-outline-secondary:hover {
|
||||||
|
background-color: #5c636a;
|
||||||
|
border-color: #565e64;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alert for format detection */
|
||||||
|
.alert-sm {
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.message-text .code-block {
|
.rendered-markdown {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rendered-markdown h1 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rendered-markdown h2 {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rendered-markdown h3 {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rendered-markdown pre {
|
||||||
|
padding: 0.75rem;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
padding: 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-text .formatted-list {
|
.rendered-markdown table {
|
||||||
padding-left: 1.2rem;
|
font-size: 0.875rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-text h3,
|
.rendered-markdown ul,
|
||||||
.message-text h4,
|
.rendered-markdown ol {
|
||||||
.message-text h5 {
|
padding-left: 1.5rem;
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
871
server.js
871
server.js
@@ -17,6 +17,15 @@ const mammoth = require('mammoth'); // For .docx files
|
|||||||
const pdfParse = require('pdf-parse'); // For PDF files
|
const pdfParse = require('pdf-parse'); // For PDF files
|
||||||
const ExcelJS = require('exceljs'); // For Excel files
|
const ExcelJS = require('exceljs'); // For Excel files
|
||||||
|
|
||||||
|
// Markdown and HTML processing
|
||||||
|
const { marked } = require('marked');
|
||||||
|
const createDOMPurify = require('dompurify');
|
||||||
|
const { JSDOM } = require('jsdom');
|
||||||
|
|
||||||
|
// Initialize DOMPurify
|
||||||
|
const window = new JSDOM('').window;
|
||||||
|
const DOMPurify = createDOMPurify(window);
|
||||||
|
|
||||||
// Helper function to extract text from various document formats
|
// Helper function to extract text from various document formats
|
||||||
async function extractTextFromDocument(filePath, fileExtension) {
|
async function extractTextFromDocument(filePath, fileExtension) {
|
||||||
try {
|
try {
|
||||||
@@ -705,6 +714,82 @@ app.get('/logout', (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Dashboard route
|
||||||
|
app.get('/dashboard', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Load user files from both session and persistent storage
|
||||||
|
let allFiles = [];
|
||||||
|
let revisedFiles = [];
|
||||||
|
|
||||||
|
// Load persistent files
|
||||||
|
try {
|
||||||
|
const persistentFiles = await loadUserFiles(req.session.userId);
|
||||||
|
const persistentRevisedFiles = await loadRevisedFiles(req.session.userId);
|
||||||
|
|
||||||
|
allFiles = persistentFiles;
|
||||||
|
revisedFiles = persistentRevisedFiles;
|
||||||
|
|
||||||
|
// Merge with session data (session data is more current)
|
||||||
|
const sessionFiles = req.session.uploadedFiles || [];
|
||||||
|
const sessionRevisedFiles = req.session.revisedFiles || [];
|
||||||
|
|
||||||
|
// Update session with persistent data if session is empty
|
||||||
|
if (sessionFiles.length === 0 && persistentFiles.length > 0) {
|
||||||
|
req.session.uploadedFiles = persistentFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sessionRevisedFiles.length === 0 && persistentRevisedFiles.length > 0) {
|
||||||
|
req.session.revisedFiles = persistentRevisedFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use session data as the source of truth for display
|
||||||
|
allFiles = req.session.uploadedFiles || persistentFiles;
|
||||||
|
revisedFiles = req.session.revisedFiles || persistentRevisedFiles;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading user files for dashboard:', error);
|
||||||
|
// Fall back to session data
|
||||||
|
allFiles = req.session.uploadedFiles || [];
|
||||||
|
revisedFiles = req.session.revisedFiles || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
res.render('dashboard', {
|
||||||
|
title: 'Dashboard - EduCat',
|
||||||
|
files: allFiles,
|
||||||
|
revisedFiles: revisedFiles
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Dashboard error:', error);
|
||||||
|
req.flash('error', 'Error loading dashboard');
|
||||||
|
res.redirect('/');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Chat route
|
||||||
|
app.get('/chat', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
// Load user chat history
|
||||||
|
let chatHistory = [];
|
||||||
|
try {
|
||||||
|
chatHistory = await loadChatHistory(req.session.userId);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading chat history:', error);
|
||||||
|
chatHistory = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
res.render('chat', {
|
||||||
|
title: 'AI Chat - EduCat',
|
||||||
|
chatHistory: chatHistory
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Chat route error:', error);
|
||||||
|
res.render('chat', {
|
||||||
|
title: 'AI Chat - EduCat',
|
||||||
|
chatHistory: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
app.get('/upload', requireAuth, (req, res) => {
|
app.get('/upload', requireAuth, (req, res) => {
|
||||||
res.render('upload', {
|
res.render('upload', {
|
||||||
title: 'Upload Your Notes - EduCat'
|
title: 'Upload Your Notes - EduCat'
|
||||||
@@ -995,6 +1080,82 @@ app.post('/api/revise', requireAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Chat API endpoint
|
||||||
|
app.post('/api/chat', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { message, history = [] } = req.body;
|
||||||
|
|
||||||
|
if (!message || message.trim().length === 0) {
|
||||||
|
return res.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Message is required'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load existing chat history from storage
|
||||||
|
let existingHistory = [];
|
||||||
|
try {
|
||||||
|
existingHistory = await loadChatHistory(req.session.userId);
|
||||||
|
} catch (error) {
|
||||||
|
console.log('No existing chat history found, starting fresh');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare history for API call (last 10 conversations)
|
||||||
|
const recentHistory = existingHistory.slice(-10).map(conv => [
|
||||||
|
{ role: 'human', content: conv.human },
|
||||||
|
{ role: 'ai', content: conv.ai }
|
||||||
|
]).flat();
|
||||||
|
|
||||||
|
// Call Flowise API for chat
|
||||||
|
const response = await axios.post(`${FLOWISE_API_URL}/${FLOWISE_CHATFLOW_ID}`, {
|
||||||
|
question: message.trim(),
|
||||||
|
history: recentHistory
|
||||||
|
});
|
||||||
|
|
||||||
|
const botResponse = response.data.text || response.data.answer || 'Sorry, I could not process your request.';
|
||||||
|
|
||||||
|
// Save the conversation to history
|
||||||
|
const conversation = {
|
||||||
|
human: message.trim(),
|
||||||
|
ai: botResponse,
|
||||||
|
timestamp: new Date().toISOString()
|
||||||
|
};
|
||||||
|
|
||||||
|
existingHistory.push(conversation);
|
||||||
|
await saveChatHistory(req.session.userId, existingHistory);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
response: botResponse
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Chat error:', error);
|
||||||
|
res.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to get response from AI. Please try again.',
|
||||||
|
details: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Delete chat history endpoint
|
||||||
|
app.delete('/api/chat/history', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
await clearChatHistory(req.session.userId);
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Chat history cleared successfully'
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error clearing chat history:', error);
|
||||||
|
res.json({
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to clear chat history',
|
||||||
|
details: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Save revised notes endpoint
|
// Save revised notes endpoint
|
||||||
app.post('/api/save-revised', requireAuth, async (req, res) => {
|
app.post('/api/save-revised', requireAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -1177,6 +1338,56 @@ app.delete('/api/revised-files/:fileId', requireAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get revised file content with rendering options
|
||||||
|
app.get('/api/revised-files/:fileId/content', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const fileId = req.params.fileId;
|
||||||
|
const { displayMode = 'markdown' } = req.query;
|
||||||
|
const revisedFiles = req.session.revisedFiles || [];
|
||||||
|
const file = revisedFiles.find(f => f.id === fileId);
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
// Try to load from persistent storage
|
||||||
|
const persistentRevisedFiles = await loadRevisedFiles(req.session.userId);
|
||||||
|
const persistentFile = persistentRevisedFiles.find(f => f.id === fileId);
|
||||||
|
|
||||||
|
if (!persistentFile) {
|
||||||
|
return res.status(404).json({
|
||||||
|
success: false,
|
||||||
|
error: 'File not found'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read file content
|
||||||
|
const content = await fs.readFile(persistentFile.path, 'utf8');
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
file: persistentFile,
|
||||||
|
content: content,
|
||||||
|
displayMode: displayMode
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read file content
|
||||||
|
const content = await fs.readFile(file.path, 'utf8');
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
success: true,
|
||||||
|
file: file,
|
||||||
|
content: content,
|
||||||
|
displayMode: displayMode
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting revised file content:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to get file content',
|
||||||
|
details: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Get revised file info endpoint
|
// Get revised file info endpoint
|
||||||
app.get('/api/revised-files/:fileId/info', requireAuth, async (req, res) => {
|
app.get('/api/revised-files/:fileId/info', requireAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -1216,200 +1427,67 @@ app.get('/api/revised-files/:fileId/info', requireAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ChatGPT integration routes
|
// Render revised notes content endpoint
|
||||||
app.get('/chat', requireAuth, (req, res) => {
|
app.post('/api/render-revised-content', requireAuth, async (req, res) => {
|
||||||
// Initialize chat history if it doesn't exist
|
|
||||||
if (!req.session.chatHistory) {
|
|
||||||
req.session.chatHistory = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize chat session ID if it doesn't exist
|
|
||||||
if (!req.session.chatSessionId) {
|
|
||||||
req.session.chatSessionId = `educat-${req.session.userId}-${Date.now()}`;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
res.render('chat', {
|
|
||||||
title: 'Chat with EduCat AI',
|
|
||||||
chatHistory: req.session.chatHistory
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
app.post('/api/chat', requireAuth, async (req, res) => {
|
|
||||||
try {
|
try {
|
||||||
const { message } = req.body;
|
const { content, displayMode = 'markdown', autoDetect = true } = req.body;
|
||||||
|
|
||||||
// Initialize chat history in session if it doesn't exist
|
if (!content) {
|
||||||
if (!req.session.chatHistory) {
|
return res.json({
|
||||||
req.session.chatHistory = [];
|
success: false,
|
||||||
|
error: 'No content provided'
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize or get persistent chat session ID for this user
|
|
||||||
if (!req.session.chatSessionId) {
|
|
||||||
req.session.chatSessionId = `${req.session.userId}-${Date.now()}`;
|
|
||||||
|
|
||||||
|
let renderedContent = '';
|
||||||
|
let detectedFormat = 'text';
|
||||||
|
let isMarkdownContent = false;
|
||||||
|
|
||||||
|
// Auto-detect if content is markdown (if autoDetect is enabled)
|
||||||
|
if (autoDetect) {
|
||||||
|
isMarkdownContent = isLikelyMarkdown(content);
|
||||||
|
detectedFormat = isMarkdownContent ? 'markdown' : 'text';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Prepare the request payload for Flowise with sessionId and chatId
|
|
||||||
const flowisePayload = {
|
|
||||||
question: message,
|
|
||||||
sessionId: req.session.chatSessionId
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add chatId if we have one from previous conversations
|
|
||||||
if (req.session.chatId) {
|
|
||||||
flowisePayload.chatId = req.session.chatId;
|
|
||||||
|
|
||||||
|
// Process content based on display mode
|
||||||
|
switch (displayMode) {
|
||||||
|
case 'html':
|
||||||
|
if (isMarkdownContent || autoDetect === false) {
|
||||||
|
// Convert markdown to safe HTML
|
||||||
|
renderedContent = markdownToSafeHtml(content);
|
||||||
|
} else {
|
||||||
|
// Just escape HTML and preserve line breaks for plain text
|
||||||
|
renderedContent = escapeHtml(content).replace(/\n/g, '<br>');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'markdown':
|
||||||
|
case 'raw':
|
||||||
|
default:
|
||||||
|
// Return raw content (for markdown view or plain text)
|
||||||
|
renderedContent = content;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Call Flowise API for chat with session history and sessionId
|
|
||||||
const response = await axios.post(`${FLOWISE_API_URL}/${FLOWISE_CHATFLOW_ID}`, flowisePayload);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const aiResponse = response.data.text || response.data.answer || 'No response received';
|
|
||||||
|
|
||||||
// Save the chatId from Flowise response for future requests
|
|
||||||
if (response.data.chatId) {
|
|
||||||
req.session.chatId = response.data.chatId;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the conversation to session history
|
|
||||||
req.session.chatHistory.push({
|
|
||||||
human: message,
|
|
||||||
ai: aiResponse
|
|
||||||
});
|
|
||||||
|
|
||||||
// Save session explicitly since we modified it
|
|
||||||
req.session.save((err) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Error saving chat session:', err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
response: aiResponse
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Chat error:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Failed to get chat response',
|
|
||||||
details: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Get chat history from session
|
|
||||||
app.get('/api/chat/history', requireAuth, (req, res) => {
|
|
||||||
try {
|
|
||||||
const chatHistory = req.session.chatHistory || [];
|
|
||||||
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
history: chatHistory
|
renderedContent: renderedContent,
|
||||||
|
displayMode: displayMode,
|
||||||
|
detectedFormat: detectedFormat,
|
||||||
|
isMarkdownContent: isMarkdownContent
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting chat history:', error);
|
console.error('Error rendering content:', error);
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Failed to get chat history',
|
error: 'Failed to render content',
|
||||||
details: error.message
|
details: error.message
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clear chat history
|
// Get file preview endpoint (simplified for dashboard)
|
||||||
app.delete('/api/chat/history', requireAuth, (req, res) => {
|
|
||||||
try {
|
|
||||||
req.session.chatHistory = [];
|
|
||||||
// Reset the session ID to start a fresh conversation
|
|
||||||
req.session.chatSessionId = `${req.session.userId}-${Date.now()}`;
|
|
||||||
// Clear the Flowise chatId
|
|
||||||
delete req.session.chatId;
|
|
||||||
|
|
||||||
req.session.save((err) => {
|
|
||||||
if (err) {
|
|
||||||
console.error('Error clearing chat session:', err);
|
|
||||||
return res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Failed to clear chat history'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
message: 'Chat history cleared'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error clearing chat history:', error);
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: 'Failed to clear chat history',
|
|
||||||
details: error.message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
app.get('/dashboard', requireAuth, async (req, res) => {
|
|
||||||
try {
|
|
||||||
// Load persistent files for this user
|
|
||||||
const persistentFiles = await loadUserFiles(req.session.userId);
|
|
||||||
|
|
||||||
// Load revised files separately
|
|
||||||
const revisedFiles = await loadRevisedFiles(req.session.userId);
|
|
||||||
|
|
||||||
// Merge with session files (in case there are newly uploaded files not yet saved)
|
|
||||||
const sessionFiles = req.session.uploadedFiles || [];
|
|
||||||
const allFiles = [...persistentFiles];
|
|
||||||
|
|
||||||
// Add any session files that aren't already in persistent storage
|
|
||||||
sessionFiles.forEach(sessionFile => {
|
|
||||||
if (!persistentFiles.find(f => f.id === sessionFile.id)) {
|
|
||||||
allFiles.push(sessionFile);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Merge revised files from session
|
|
||||||
const sessionRevisedFiles = req.session.revisedFiles || [];
|
|
||||||
const allRevisedFiles = [...revisedFiles];
|
|
||||||
sessionRevisedFiles.forEach(sessionFile => {
|
|
||||||
if (!revisedFiles.find(f => f.id === sessionFile.id)) {
|
|
||||||
allRevisedFiles.push(sessionFile);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Update session with merged files for current session use
|
|
||||||
req.session.uploadedFiles = allFiles;
|
|
||||||
req.session.revisedFiles = allRevisedFiles;
|
|
||||||
|
|
||||||
res.render('dashboard', {
|
|
||||||
title: 'Dashboard - EduCat',
|
|
||||||
files: allFiles,
|
|
||||||
revisedFiles: allRevisedFiles
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error loading dashboard:', error);
|
|
||||||
res.render('dashboard', {
|
|
||||||
title: 'Dashboard - EduCat',
|
|
||||||
files: req.session.uploadedFiles || [],
|
|
||||||
revisedFiles: []
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// File management endpoints
|
|
||||||
app.get('/api/files/:fileId/preview', requireAuth, async (req, res) => {
|
app.get('/api/files/:fileId/preview', requireAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const fileId = req.params.fileId;
|
const fileId = req.params.fileId;
|
||||||
@@ -1417,12 +1495,102 @@ app.get('/api/files/:fileId/preview', requireAuth, async (req, res) => {
|
|||||||
const file = files.find(f => f.id === fileId);
|
const file = files.find(f => f.id === fileId);
|
||||||
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
return res.status(404).json({ success: false, error: 'File not found' });
|
// Try to load from persistent storage
|
||||||
|
try {
|
||||||
|
const persistentFiles = await loadUserFiles(req.session.userId);
|
||||||
|
const persistentFile = persistentFiles.find(f => f.id === fileId);
|
||||||
|
|
||||||
|
if (!persistentFile) {
|
||||||
|
return res.status(404).json({ success: false, error: 'File not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the persistent file
|
||||||
|
const filePath = path.join(__dirname, persistentFile.path);
|
||||||
|
const fileExtension = path.extname(persistentFile.originalName).toLowerCase();
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
if (!await fs.pathExists(filePath)) {
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
file: {
|
||||||
|
id: persistentFile.id,
|
||||||
|
originalName: persistentFile.originalName,
|
||||||
|
size: persistentFile.size,
|
||||||
|
uploadDate: persistentFile.uploadDate,
|
||||||
|
content: 'File not found on disk',
|
||||||
|
previewType: 'error',
|
||||||
|
message: `File "${persistentFile.originalName}" was uploaded but the physical file is no longer available on disk. Please re-upload the file.`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to extract text from the document
|
||||||
|
const extractionResult = await extractTextFromDocument(filePath, fileExtension);
|
||||||
|
|
||||||
|
if (extractionResult.success) {
|
||||||
|
const extractedText = extractionResult.text;
|
||||||
|
const previewContent = extractedText.length > 5000 ?
|
||||||
|
extractedText.substring(0, 5000) + '\n\n... (content truncated for preview)' :
|
||||||
|
extractedText;
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
file: {
|
||||||
|
id: persistentFile.id,
|
||||||
|
originalName: persistentFile.originalName,
|
||||||
|
size: persistentFile.size,
|
||||||
|
uploadDate: persistentFile.uploadDate,
|
||||||
|
content: previewContent,
|
||||||
|
previewType: 'extracted-text',
|
||||||
|
extractionInfo: {
|
||||||
|
method: extractionResult.method,
|
||||||
|
totalLength: extractionResult.extractedLength,
|
||||||
|
pages: extractionResult.pages || null,
|
||||||
|
sheets: extractionResult.sheets || null,
|
||||||
|
truncated: extractedText.length > 5000
|
||||||
|
},
|
||||||
|
message: `Text successfully extracted from ${fileExtension.toUpperCase()} file. ${extractedText.length > 5000 ? 'Preview truncated to first 5000 characters.' : ''}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
file: {
|
||||||
|
id: persistentFile.id,
|
||||||
|
originalName: persistentFile.originalName,
|
||||||
|
size: persistentFile.size,
|
||||||
|
uploadDate: persistentFile.uploadDate,
|
||||||
|
content: 'Preview not available for this file type. File content is available for AI processing.',
|
||||||
|
previewType: 'extraction-failed',
|
||||||
|
message: `Failed to extract text from ${fileExtension.toUpperCase()} file: ${extractionResult.error}. The file has been uploaded and may still be usable for AI processing.`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading from persistent storage:', error);
|
||||||
|
return res.status(404).json({ success: false, error: 'File not found' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const filePath = path.join(__dirname, file.path);
|
const filePath = path.join(__dirname, file.path);
|
||||||
const fileExtension = path.extname(file.originalName).toLowerCase();
|
const fileExtension = path.extname(file.originalName).toLowerCase();
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
if (!await fs.pathExists(filePath)) {
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
file: {
|
||||||
|
id: file.id,
|
||||||
|
originalName: file.originalName,
|
||||||
|
size: file.size,
|
||||||
|
uploadDate: file.uploadDate,
|
||||||
|
content: 'File not found on disk',
|
||||||
|
previewType: 'error',
|
||||||
|
message: `File "${file.originalName}" was uploaded but the physical file is no longer available on disk. Please re-upload the file.`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Try to extract text from the document
|
// Try to extract text from the document
|
||||||
const extractionResult = await extractTextFromDocument(filePath, fileExtension);
|
const extractionResult = await extractTextFromDocument(filePath, fileExtension);
|
||||||
|
|
||||||
@@ -1435,7 +1603,7 @@ app.get('/api/files/:fileId/preview', requireAuth, async (req, res) => {
|
|||||||
extractedText.substring(0, 5000) + '\n\n... (content truncated for preview)' :
|
extractedText.substring(0, 5000) + '\n\n... (content truncated for preview)' :
|
||||||
extractedText;
|
extractedText;
|
||||||
|
|
||||||
res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
file: {
|
file: {
|
||||||
id: file.id,
|
id: file.id,
|
||||||
@@ -1457,7 +1625,6 @@ app.get('/api/files/:fileId/preview', requireAuth, async (req, res) => {
|
|||||||
} else {
|
} else {
|
||||||
// Failed to extract text, fall back to file type detection
|
// Failed to extract text, fall back to file type detection
|
||||||
const textFormats = ['.txt', '.md', '.json', '.js', '.html', '.css', '.xml', '.csv'];
|
const textFormats = ['.txt', '.md', '.json', '.js', '.html', '.css', '.xml', '.csv'];
|
||||||
const binaryFormats = ['.pdf', '.docx', '.doc', '.xlsx', '.xls', '.pptx', '.ppt'];
|
|
||||||
|
|
||||||
if (textFormats.includes(fileExtension)) {
|
if (textFormats.includes(fileExtension)) {
|
||||||
// Try reading as plain text (should have been handled by extraction, but fallback)
|
// Try reading as plain text (should have been handled by extraction, but fallback)
|
||||||
@@ -1467,7 +1634,7 @@ app.get('/api/files/:fileId/preview', requireAuth, async (req, res) => {
|
|||||||
fileContent.substring(0, 5000) + '\n\n... (truncated)' :
|
fileContent.substring(0, 5000) + '\n\n... (truncated)' :
|
||||||
fileContent;
|
fileContent;
|
||||||
|
|
||||||
res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
file: {
|
file: {
|
||||||
id: file.id,
|
id: file.id,
|
||||||
@@ -1479,7 +1646,7 @@ app.get('/api/files/:fileId/preview', requireAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (readError) {
|
} catch (readError) {
|
||||||
res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
file: {
|
file: {
|
||||||
id: file.id,
|
id: file.id,
|
||||||
@@ -1494,14 +1661,213 @@ app.get('/api/files/:fileId/preview', requireAuth, async (req, res) => {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Binary format that couldn't be processed
|
// Binary format that couldn't be processed
|
||||||
res.json({
|
return res.json({
|
||||||
success: true,
|
success: true,
|
||||||
file: {
|
file: {
|
||||||
id: file.id,
|
id: file.id,
|
||||||
originalName: file.originalName,
|
originalName: file.originalName,
|
||||||
size: file.size,
|
size: file.size,
|
||||||
uploadDate: file.uploadDate,
|
uploadDate: file.uploadDate,
|
||||||
content: 'Text extraction failed',
|
content: 'Preview not available for this file type. File content is available for AI processing.',
|
||||||
|
previewType: 'extraction-failed',
|
||||||
|
message: `Failed to extract text from ${fileExtension.toUpperCase()} file: ${extractionResult.error}. The file has been uploaded and may still be usable for AI processing.`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error getting file preview:', error);
|
||||||
|
res.status(500).json({
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to get file preview',
|
||||||
|
details: error.message
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get file preview content endpoint (for notes and revisions)
|
||||||
|
app.get('/api/files/:fileId/preview-content', requireAuth, async (req, res) => {
|
||||||
|
try {
|
||||||
|
const fileId = req.params.fileId;
|
||||||
|
const { displayMode = 'markdown' } = req.query;
|
||||||
|
const files = req.session.uploadedFiles || [];
|
||||||
|
const file = files.find(f => f.id === fileId);
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
// Try to load from persistent storage
|
||||||
|
try {
|
||||||
|
const persistentFiles = await loadUserFiles(req.session.userId);
|
||||||
|
const persistentFile = persistentFiles.find(f => f.id === fileId);
|
||||||
|
|
||||||
|
if (!persistentFile) {
|
||||||
|
return res.status(404).json({ success: false, error: 'File not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the persistent file
|
||||||
|
const filePath = path.join(__dirname, persistentFile.path);
|
||||||
|
const fileExtension = path.extname(persistentFile.originalName).toLowerCase();
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
if (!await fs.pathExists(filePath)) {
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
file: {
|
||||||
|
id: persistentFile.id,
|
||||||
|
originalName: persistentFile.originalName,
|
||||||
|
size: persistentFile.size,
|
||||||
|
uploadDate: persistentFile.uploadDate,
|
||||||
|
content: 'File not found on disk',
|
||||||
|
previewType: 'error',
|
||||||
|
message: `File "${persistentFile.originalName}" was uploaded but the physical file is no longer available on disk. Please re-upload the file.`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to extract text from the document
|
||||||
|
const extractionResult = await extractTextFromDocument(filePath, fileExtension);
|
||||||
|
|
||||||
|
if (extractionResult.success) {
|
||||||
|
const extractedText = extractionResult.text;
|
||||||
|
const previewContent = extractedText.length > 5000 ?
|
||||||
|
extractedText.substring(0, 5000) + '\n\n... (content truncated for preview)' :
|
||||||
|
extractedText;
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
file: {
|
||||||
|
id: persistentFile.id,
|
||||||
|
originalName: persistentFile.originalName,
|
||||||
|
size: persistentFile.size,
|
||||||
|
uploadDate: persistentFile.uploadDate,
|
||||||
|
content: previewContent,
|
||||||
|
previewType: 'extracted-text',
|
||||||
|
extractionInfo: {
|
||||||
|
method: extractionResult.method,
|
||||||
|
totalLength: extractionResult.extractedLength,
|
||||||
|
pages: extractionResult.pages || null,
|
||||||
|
sheets: extractionResult.sheets || null,
|
||||||
|
truncated: extractedText.length > 5000
|
||||||
|
},
|
||||||
|
message: `Text successfully extracted from ${fileExtension.toUpperCase()} file. ${extractedText.length > 5000 ? 'Preview truncated to first 5000 characters.' : ''}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
file: {
|
||||||
|
id: persistentFile.id,
|
||||||
|
originalName: persistentFile.originalName,
|
||||||
|
size: persistentFile.size,
|
||||||
|
uploadDate: persistentFile.uploadDate,
|
||||||
|
content: 'Preview not available for this file type. File content is available for AI processing.',
|
||||||
|
previewType: 'extraction-failed',
|
||||||
|
message: `Failed to extract text from ${fileExtension.toUpperCase()} file: ${extractionResult.error}. The file has been uploaded and may still be usable for AI processing.`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading from persistent storage:', error);
|
||||||
|
return res.status(404).json({ success: false, error: 'File not found' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const filePath = path.join(__dirname, file.path);
|
||||||
|
const fileExtension = path.extname(file.originalName).toLowerCase();
|
||||||
|
|
||||||
|
// Check if file exists
|
||||||
|
if (!await fs.pathExists(filePath)) {
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
file: {
|
||||||
|
id: file.id,
|
||||||
|
originalName: file.originalName,
|
||||||
|
size: file.size,
|
||||||
|
uploadDate: file.uploadDate,
|
||||||
|
content: 'File not found on disk',
|
||||||
|
previewType: 'error',
|
||||||
|
message: `File "${file.originalName}" was uploaded but the physical file is no longer available on disk. Please re-upload the file.`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to extract text from the document
|
||||||
|
const extractionResult = await extractTextFromDocument(filePath, fileExtension);
|
||||||
|
|
||||||
|
if (extractionResult.success) {
|
||||||
|
// Successfully extracted text
|
||||||
|
const extractedText = extractionResult.text;
|
||||||
|
|
||||||
|
// Limit preview to first 5000 characters to avoid huge responses
|
||||||
|
const previewContent = extractedText.length > 5000 ?
|
||||||
|
extractedText.substring(0, 5000) + '\n\n... (content truncated for preview)' :
|
||||||
|
extractedText;
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
file: {
|
||||||
|
id: file.id,
|
||||||
|
originalName: file.originalName,
|
||||||
|
size: file.size,
|
||||||
|
uploadDate: file.uploadDate,
|
||||||
|
content: previewContent,
|
||||||
|
previewType: 'extracted-text',
|
||||||
|
extractionInfo: {
|
||||||
|
method: extractionResult.method,
|
||||||
|
totalLength: extractionResult.extractedLength,
|
||||||
|
pages: extractionResult.pages || null,
|
||||||
|
sheets: extractionResult.sheets || null,
|
||||||
|
truncated: extractedText.length > 5000
|
||||||
|
},
|
||||||
|
message: `Text successfully extracted from ${fileExtension.toUpperCase()} file. ${extractedText.length > 5000 ? 'Preview truncated to first 5000 characters.' : ''}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Failed to extract text, fall back to file type detection
|
||||||
|
const textFormats = ['.txt', '.md', '.json', '.js', '.html', '.css', '.xml', '.csv'];
|
||||||
|
|
||||||
|
if (textFormats.includes(fileExtension)) {
|
||||||
|
// Try reading as plain text (should have been handled by extraction, but fallback)
|
||||||
|
try {
|
||||||
|
const fileContent = await fs.readFile(filePath, 'utf-8');
|
||||||
|
const previewContent = fileContent.length > 5000 ?
|
||||||
|
fileContent.substring(0, 5000) + '\n\n... (truncated)' :
|
||||||
|
fileContent;
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
file: {
|
||||||
|
id: file.id,
|
||||||
|
originalName: file.originalName,
|
||||||
|
size: file.size,
|
||||||
|
uploadDate: file.uploadDate,
|
||||||
|
content: previewContent,
|
||||||
|
previewType: 'text'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (readError) {
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
file: {
|
||||||
|
id: file.id,
|
||||||
|
originalName: file.originalName,
|
||||||
|
size: file.size,
|
||||||
|
uploadDate: file.uploadDate,
|
||||||
|
content: 'File preview not available',
|
||||||
|
previewType: 'error',
|
||||||
|
message: `Error reading ${fileExtension.toUpperCase()} file: ${readError.message}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Binary format that couldn't be processed
|
||||||
|
return res.json({
|
||||||
|
success: true,
|
||||||
|
file: {
|
||||||
|
id: file.id,
|
||||||
|
originalName: file.originalName,
|
||||||
|
size: file.size,
|
||||||
|
uploadDate: file.uploadDate,
|
||||||
|
content: 'Preview not available for this file type. File content is available for AI processing.',
|
||||||
previewType: 'extraction-failed',
|
previewType: 'extraction-failed',
|
||||||
message: `Failed to extract text from ${fileExtension.toUpperCase()} file: ${extractionResult.error}. The file has been uploaded and may still be usable for AI processing.`
|
message: `Failed to extract text from ${fileExtension.toUpperCase()} file: ${extractionResult.error}. The file has been uploaded and may still be usable for AI processing.`
|
||||||
}
|
}
|
||||||
@@ -2338,6 +2704,147 @@ app.listen(PORT, () => {
|
|||||||
console.log(`EduCat server running on http://localhost:${PORT}`);
|
console.log(`EduCat server running on http://localhost:${PORT}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Helper function to convert markdown to safe HTML
|
||||||
|
function markdownToSafeHtml(markdownText) {
|
||||||
|
try {
|
||||||
|
// Configure marked options for better security and features
|
||||||
|
marked.setOptions({
|
||||||
|
gfm: true, // GitHub Flavored Markdown
|
||||||
|
breaks: true, // Convert line breaks to <br>
|
||||||
|
sanitize: false, // We'll use DOMPurify instead for better control
|
||||||
|
smartLists: true,
|
||||||
|
smartypants: true,
|
||||||
|
highlight: null // No code highlighting to avoid security issues
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert markdown to HTML
|
||||||
|
const rawHtml = marked.parse(markdownText);
|
||||||
|
|
||||||
|
// Sanitize the HTML with DOMPurify
|
||||||
|
const cleanHtml = DOMPurify.sanitize(rawHtml, {
|
||||||
|
ALLOWED_TAGS: [
|
||||||
|
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
||||||
|
'p', 'br', 'hr',
|
||||||
|
'strong', 'b', 'em', 'i', 'u', 'mark',
|
||||||
|
'ul', 'ol', 'li',
|
||||||
|
'blockquote', 'pre', 'code',
|
||||||
|
'table', 'thead', 'tbody', 'tr', 'th', 'td',
|
||||||
|
'a', 'img',
|
||||||
|
'div', 'span'
|
||||||
|
],
|
||||||
|
ALLOWED_ATTR: [
|
||||||
|
'href', 'target', 'rel',
|
||||||
|
'src', 'alt', 'title',
|
||||||
|
'class', 'id',
|
||||||
|
'width', 'height'
|
||||||
|
],
|
||||||
|
ALLOW_DATA_ATTR: false,
|
||||||
|
FORBID_TAGS: ['script', 'object', 'embed', 'iframe', 'form', 'input'],
|
||||||
|
FORBID_ATTR: ['onclick', 'onload', 'onerror', 'style'],
|
||||||
|
ADD_ATTR: {
|
||||||
|
'a': { 'target': '_blank', 'rel': 'noopener noreferrer' }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return cleanHtml;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error converting markdown to HTML:', error);
|
||||||
|
// Return escaped plain text as fallback
|
||||||
|
return escapeHtml(markdownText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to escape HTML
|
||||||
|
function escapeHtml(text) {
|
||||||
|
const map = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": '''
|
||||||
|
};
|
||||||
|
return text.replace(/[&<>"']/g, function(m) { return map[m]; });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to detect if content is likely markdown
|
||||||
|
function isLikelyMarkdown(text) {
|
||||||
|
const markdownIndicators = [
|
||||||
|
/^#{1,6}\s+.+$/m, // Headers
|
||||||
|
/\*{1,2}[^*]+\*{1,2}/, // Bold/italic
|
||||||
|
/^[\s]*[-*+]\s+/m, // Bullet lists
|
||||||
|
/^\d+\.\s+/m, // Numbered lists
|
||||||
|
/```[\s\S]*?```/, // Code blocks
|
||||||
|
/`[^`]+`/, // Inline code
|
||||||
|
/\[.+\]\(.+\)/, // Links
|
||||||
|
/^\>.+$/m // Blockquotes
|
||||||
|
];
|
||||||
|
|
||||||
|
return markdownIndicators.some(pattern => pattern.test(text));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chat history storage persistence
|
||||||
|
const CHAT_HISTORY_DIR = path.join(__dirname, 'data', 'chat-history');
|
||||||
|
|
||||||
|
// Ensure chat history directory exists
|
||||||
|
async function ensureChatHistoryDirectory() {
|
||||||
|
await fs.ensureDir(CHAT_HISTORY_DIR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save chat history to persistent storage
|
||||||
|
async function saveChatHistory(userId, chatHistory) {
|
||||||
|
try {
|
||||||
|
await ensureChatHistoryDirectory();
|
||||||
|
const historyPath = path.join(CHAT_HISTORY_DIR, `chat-${userId}.json`);
|
||||||
|
await fs.writeJSON(historyPath, chatHistory, { spaces: 2 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving chat history:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load chat history from persistent storage
|
||||||
|
async function loadChatHistory(userId) {
|
||||||
|
try {
|
||||||
|
await ensureChatHistoryDirectory();
|
||||||
|
const historyPath = path.join(CHAT_HISTORY_DIR, `chat-${userId}.json`);
|
||||||
|
|
||||||
|
if (await fs.pathExists(historyPath)) {
|
||||||
|
return await fs.readJSON(historyPath);
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading chat history:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear chat history for a user
|
||||||
|
async function clearChatHistory(userId) {
|
||||||
|
try {
|
||||||
|
await ensureChatHistoryDirectory();
|
||||||
|
const historyPath = path.join(CHAT_HISTORY_DIR, `chat-${userId}.json`);
|
||||||
|
|
||||||
|
if (await fs.pathExists(historyPath)) {
|
||||||
|
await fs.unlink(historyPath);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error clearing chat history:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize directory structures on startup
|
||||||
|
async function initializeDataDirectories() {
|
||||||
|
await ensureUserFilesDirectory();
|
||||||
|
await ensureRevisedFilesDirectory();
|
||||||
|
await ensureQuizResultsDirectory();
|
||||||
|
await ensureChatHistoryDirectory();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call initialization
|
||||||
|
initializeDataDirectories().catch(console.error);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -101,12 +101,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
• Explanations of complex concepts
|
• Explanations of complex concepts
|
||||||
• Creating study plans and schedules
|
• Creating study plans and schedules
|
||||||
|
|
||||||
I support **rich formatting** in my responses including:
|
|
||||||
- **Bold text** and *italic text*
|
|
||||||
- \`Code snippets\` and code blocks
|
|
||||||
- Numbered lists and bullet points
|
|
||||||
- Headers and structured content
|
|
||||||
|
|
||||||
How can I assist you today?`;
|
How can I assist you today?`;
|
||||||
const formattedWelcome = formatMessage(welcomeText, true);
|
const formattedWelcome = formatMessage(welcomeText, true);
|
||||||
|
|
||||||
|
|||||||
@@ -890,25 +890,100 @@ async function previewRevisedFile(fileId) {
|
|||||||
|
|
||||||
const file = fileInfo.file;
|
const file = fileInfo.file;
|
||||||
|
|
||||||
// Download the file content
|
// Get file content using the new API endpoint
|
||||||
const contentResponse = await fetch(`/uploads/revised-notes/${file.filename}`);
|
const contentResponse = await fetch(`/api/revised-files/${fileId}/content`);
|
||||||
if (!contentResponse.ok) {
|
if (!contentResponse.ok) {
|
||||||
throw new Error('Failed to load file content');
|
throw new Error('Failed to load file content');
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = await contentResponse.text();
|
const contentResult = await contentResponse.json();
|
||||||
|
if (!contentResult.success) {
|
||||||
|
throw new Error('Failed to load file content');
|
||||||
|
}
|
||||||
|
|
||||||
|
const content = contentResult.content;
|
||||||
const modal = new bootstrap.Modal(document.getElementById('previewModal'));
|
const modal = new bootstrap.Modal(document.getElementById('previewModal'));
|
||||||
|
|
||||||
|
// Create preview content with display mode toggle
|
||||||
document.getElementById('preview-content').innerHTML = `
|
document.getElementById('preview-content').innerHTML = `
|
||||||
<div class="alert alert-info">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<i class="fas fa-brain me-2"></i>
|
<div class="alert alert-info mb-0 flex-grow-1 me-3">
|
||||||
<strong>AI-Revised Content</strong> • ${file.revisionType} • From: ${file.originalFileName || 'Unknown'}
|
<i class="fas fa-brain me-2"></i>
|
||||||
|
<strong>AI-Revised Content</strong> • ${file.revisionType} • From: ${file.originalFileName || 'Unknown'}
|
||||||
|
</div>
|
||||||
|
<div class="btn-group btn-group-sm" role="group" id="preview-display-mode">
|
||||||
|
<input type="radio" class="btn-check" name="previewDisplayMode" id="preview-mode-markdown" value="markdown" checked>
|
||||||
|
<label class="btn btn-outline-secondary" for="preview-mode-markdown" title="Show as Markdown">
|
||||||
|
<i class="fab fa-markdown"></i>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input type="radio" class="btn-check" name="previewDisplayMode" id="preview-mode-html" value="html">
|
||||||
|
<label class="btn btn-outline-secondary" for="preview-mode-html" title="Render as HTML">
|
||||||
|
<i class="fas fa-code"></i>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="border p-3 bg-light rounded" style="max-height: 400px; overflow-y: auto;">
|
<div id="preview-content-container" class="border p-3 bg-light rounded" style="max-height: 400px; overflow-y: auto;">
|
||||||
<pre style="white-space: pre-wrap; word-wrap: break-word;">${escapeHtml(content)}</pre>
|
<pre style="white-space: pre-wrap; word-wrap: break-word; margin: 0;">${escapeHtml(content)}</pre>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
// Add event listeners for display mode toggle
|
||||||
|
const modeMarkdown = document.getElementById('preview-mode-markdown');
|
||||||
|
const modeHtml = document.getElementById('preview-mode-html');
|
||||||
|
const contentContainer = document.getElementById('preview-content-container');
|
||||||
|
|
||||||
|
async function updatePreviewMode() {
|
||||||
|
const selectedMode = document.querySelector('input[name="previewDisplayMode"]:checked').value;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const renderResponse = await fetch('/api/render-revised-content', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
content: content,
|
||||||
|
displayMode: selectedMode,
|
||||||
|
autoDetect: true
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const renderResult = await renderResponse.json();
|
||||||
|
|
||||||
|
if (renderResult.success) {
|
||||||
|
if (selectedMode === 'html') {
|
||||||
|
let htmlContent = `<div class="rendered-markdown">${renderResult.renderedContent}</div>`;
|
||||||
|
|
||||||
|
if (renderResult.isMarkdownContent) {
|
||||||
|
htmlContent = `
|
||||||
|
<div class="alert alert-info alert-sm mb-2">
|
||||||
|
<small><i class="fas fa-info-circle me-1"></i>Markdown formatting detected and rendered</small>
|
||||||
|
</div>
|
||||||
|
${htmlContent}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
contentContainer.innerHTML = htmlContent;
|
||||||
|
contentContainer.style.backgroundColor = '#ffffff';
|
||||||
|
} else {
|
||||||
|
contentContainer.innerHTML = `<pre style="white-space: pre-wrap; word-wrap: break-word; margin: 0;">${escapeHtml(renderResult.renderedContent)}</pre>`;
|
||||||
|
contentContainer.style.backgroundColor = '#f8f9fa';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(renderResult.error || 'Failed to render content');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error rendering preview:', error);
|
||||||
|
// Fallback to escaped text
|
||||||
|
contentContainer.innerHTML = `<pre style="white-space: pre-wrap; word-wrap: break-word; margin: 0;">${escapeHtml(content)}</pre>`;
|
||||||
|
contentContainer.style.backgroundColor = '#f8f9fa';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
modeMarkdown.addEventListener('change', updatePreviewMode);
|
||||||
|
modeHtml.addEventListener('change', updatePreviewMode);
|
||||||
|
|
||||||
modal.show();
|
modal.show();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error previewing revised file:', error);
|
console.error('Error previewing revised file:', error);
|
||||||
|
|||||||
@@ -35,7 +35,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<h5>AI-Revised Notes</h5>
|
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||||
|
<h5>AI-Revised Notes</h5>
|
||||||
|
<div class="btn-group btn-group-sm" role="group" id="display-mode-toggle" style="display: none;">
|
||||||
|
<input type="radio" class="btn-check" name="displayMode" id="mode-markdown" value="markdown" checked>
|
||||||
|
<label class="btn btn-outline-secondary" for="mode-markdown" title="Show as Markdown">
|
||||||
|
<i class="fab fa-markdown"></i>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input type="radio" class="btn-check" name="displayMode" id="mode-html" value="html">
|
||||||
|
<label class="btn btn-outline-secondary" for="mode-html" title="Render as HTML">
|
||||||
|
<i class="fas fa-code"></i>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div id="revised-content" class="border p-3 bg-white rounded" style="height: 400px; overflow-y: auto;">
|
<div id="revised-content" class="border p-3 bg-white rounded" style="height: 400px; overflow-y: auto;">
|
||||||
<p class="text-muted text-center mt-5">Select a revision type and click "Revise" to see AI-enhanced notes here.</p>
|
<p class="text-muted text-center mt-5">Select a revision type and click "Revise" to see AI-enhanced notes here.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -139,11 +152,72 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const revisionType = document.getElementById('revision-type');
|
const revisionType = document.getElementById('revision-type');
|
||||||
const saveBtn = document.getElementById('save-btn');
|
const saveBtn = document.getElementById('save-btn');
|
||||||
const downloadBtn = document.getElementById('download-btn');
|
const downloadBtn = document.getElementById('download-btn');
|
||||||
|
const displayModeToggle = document.getElementById('display-mode-toggle');
|
||||||
|
const modeMarkdown = document.getElementById('mode-markdown');
|
||||||
|
const modeHtml = document.getElementById('mode-html');
|
||||||
|
|
||||||
const fileId = '<%= file.id %>';
|
const fileId = '<%= file.id %>';
|
||||||
const content = <%- JSON.stringify(content) %>;
|
const content = <%- JSON.stringify(content) %>;
|
||||||
let currentRevisedContent = '';
|
let currentRevisedContent = '';
|
||||||
let currentRevisionType = '';
|
let currentRevisionType = '';
|
||||||
|
let currentDisplayMode = 'markdown';
|
||||||
|
|
||||||
|
// Handle display mode changes
|
||||||
|
function updateDisplayMode() {
|
||||||
|
if (!currentRevisedContent) return;
|
||||||
|
|
||||||
|
const selectedMode = document.querySelector('input[name="displayMode"]:checked').value;
|
||||||
|
currentDisplayMode = selectedMode;
|
||||||
|
|
||||||
|
renderRevisedContent(currentRevisedContent, selectedMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render revised content based on display mode
|
||||||
|
async function renderRevisedContent(content, displayMode = 'markdown') {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/render-revised-content', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
content: content,
|
||||||
|
displayMode: displayMode,
|
||||||
|
autoDetect: true
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
if (displayMode === 'html') {
|
||||||
|
// Render as HTML
|
||||||
|
revisedContent.innerHTML = `<div class="rendered-markdown">${result.renderedContent}</div>`;
|
||||||
|
|
||||||
|
// Show format detection info if available
|
||||||
|
if (result.isMarkdownContent) {
|
||||||
|
const formatInfo = document.createElement('div');
|
||||||
|
formatInfo.className = 'alert alert-info alert-sm mb-2';
|
||||||
|
formatInfo.innerHTML = '<small><i class="fas fa-info-circle me-1"></i>Markdown formatting detected and rendered</small>';
|
||||||
|
revisedContent.insertBefore(formatInfo, revisedContent.firstChild);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Show as raw markdown/text
|
||||||
|
revisedContent.innerHTML = `<pre class="mb-0" style="white-space: pre-wrap; word-wrap: break-word;">${escapeHtml(result.renderedContent)}</pre>`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error(result.error || 'Failed to render content');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error rendering content:', error);
|
||||||
|
// Fallback to escaped text
|
||||||
|
revisedContent.innerHTML = `<pre class="mb-0" style="white-space: pre-wrap; word-wrap: break-word;">${escapeHtml(content)}</pre>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listeners for display mode toggle
|
||||||
|
modeMarkdown.addEventListener('change', updateDisplayMode);
|
||||||
|
modeHtml.addEventListener('change', updateDisplayMode);
|
||||||
|
|
||||||
reviseBtn.addEventListener('click', async function() {
|
reviseBtn.addEventListener('click', async function() {
|
||||||
const type = revisionType.value;
|
const type = revisionType.value;
|
||||||
@@ -153,6 +227,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
|
|
||||||
reviseBtn.disabled = true;
|
reviseBtn.disabled = true;
|
||||||
revisionProgress.classList.remove('d-none');
|
revisionProgress.classList.remove('d-none');
|
||||||
|
displayModeToggle.style.display = 'none'; // Hide toggle during processing
|
||||||
revisedContent.innerHTML = '<div class="text-center mt-5"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Processing...</span></div><p class="mt-2">AI is processing your notes...</p></div>';
|
revisedContent.innerHTML = '<div class="text-center mt-5"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Processing...</span></div><p class="mt-2">AI is processing your notes...</p></div>';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -175,9 +250,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
console.log('Revision result:', result);
|
console.log('Revision result:', result);
|
||||||
|
|
||||||
if (result.success) {
|
if (result.success) {
|
||||||
revisedContent.innerHTML = '<pre class="mb-0" style="white-space: pre-wrap; word-wrap: break-word;">' + escapeHtml(result.revisedContent) + '</pre>';
|
|
||||||
currentRevisedContent = result.revisedContent;
|
currentRevisedContent = result.revisedContent;
|
||||||
currentRevisionType = type;
|
currentRevisionType = type;
|
||||||
|
|
||||||
|
// Show display mode toggle
|
||||||
|
displayModeToggle.style.display = 'block';
|
||||||
|
|
||||||
|
// Render content based on current display mode
|
||||||
|
await renderRevisedContent(currentRevisedContent, currentDisplayMode);
|
||||||
|
|
||||||
saveBtn.disabled = false;
|
saveBtn.disabled = false;
|
||||||
downloadBtn.disabled = false;
|
downloadBtn.disabled = false;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
Reference in New Issue
Block a user