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",
|
||||
"connect-flash": "^0.1.1",
|
||||
"cors": "^2.8.5",
|
||||
"dompurify": "^3.2.6",
|
||||
"dotenv": "^16.4.5",
|
||||
"ejs": "^3.1.10",
|
||||
"exceljs": "^4.4.0",
|
||||
@@ -21,7 +22,9 @@
|
||||
"express-session": "^1.18.0",
|
||||
"form-data": "^4.0.3",
|
||||
"fs-extra": "^11.2.0",
|
||||
"jsdom": "^26.1.0",
|
||||
"mammoth": "^1.9.1",
|
||||
"marked": "^16.0.0",
|
||||
"multer": "^2.0.0",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"uuid": "^10.0.0"
|
||||
@@ -30,6 +33,129 @@
|
||||
"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": {
|
||||
"version": "4.3.5",
|
||||
"resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz",
|
||||
@@ -85,6 +211,13 @@
|
||||
"integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==",
|
||||
"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": {
|
||||
"version": "0.8.10",
|
||||
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
|
||||
@@ -794,6 +927,66 @@
|
||||
"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": {
|
||||
"version": "1.11.13",
|
||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||
@@ -809,6 +1002,12 @@
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
@@ -858,6 +1057,15 @@
|
||||
"integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==",
|
||||
"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": {
|
||||
"version": "16.6.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||
@@ -977,6 +1185,18 @@
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
@@ -1541,6 +1761,18 @@
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
|
||||
@@ -1557,6 +1789,51 @@
|
||||
"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": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||
@@ -1719,6 +1996,12 @@
|
||||
"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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
|
||||
@@ -1743,6 +2026,136 @@
|
||||
"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": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
|
||||
@@ -1944,6 +2357,12 @@
|
||||
"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": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
|
||||
@@ -1992,6 +2411,18 @@
|
||||
"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": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
@@ -2307,6 +2738,12 @@
|
||||
"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": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
@@ -2370,6 +2807,18 @@
|
||||
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
|
||||
"license": "(MIT AND Zlib)"
|
||||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
||||
"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": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
@@ -2467,6 +2916,15 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
@@ -2588,6 +3046,12 @@
|
||||
"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": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
@@ -2871,6 +3335,12 @@
|
||||
"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": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
||||
@@ -2916,6 +3386,24 @@
|
||||
"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": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
|
||||
@@ -2957,6 +3445,18 @@
|
||||
"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": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||
@@ -3119,12 +3619,57 @@
|
||||
"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": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
|
||||
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
|
||||
"license": "BSD-2-Clause"
|
||||
},
|
||||
"node_modules/whatwg-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": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||
@@ -3150,6 +3695,36 @@
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"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": {
|
||||
"version": "10.1.1",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"body-parser": "^1.20.2",
|
||||
"connect-flash": "^0.1.1",
|
||||
"cors": "^2.8.5",
|
||||
"dompurify": "^3.2.6",
|
||||
"dotenv": "^16.4.5",
|
||||
"ejs": "^3.1.10",
|
||||
"exceljs": "^4.4.0",
|
||||
@@ -20,7 +21,9 @@
|
||||
"express-session": "^1.18.0",
|
||||
"form-data": "^4.0.3",
|
||||
"fs-extra": "^11.2.0",
|
||||
"jsdom": "^26.1.0",
|
||||
"mammoth": "^1.9.1",
|
||||
"marked": "^16.0.0",
|
||||
"multer": "^2.0.0",
|
||||
"pdf-parse": "^1.1.1",
|
||||
"uuid": "^10.0.0"
|
||||
|
||||
@@ -740,20 +740,231 @@ body {
|
||||
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) {
|
||||
.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;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.message-text .formatted-list {
|
||||
padding-left: 1.2rem;
|
||||
.rendered-markdown table {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.message-text h3,
|
||||
.message-text h4,
|
||||
.message-text h5 {
|
||||
font-size: 0.95rem;
|
||||
.rendered-markdown ul,
|
||||
.rendered-markdown ol {
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
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 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
|
||||
async function extractTextFromDocument(filePath, fileExtension) {
|
||||
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) => {
|
||||
res.render('upload', {
|
||||
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
|
||||
app.post('/api/save-revised', requireAuth, async (req, res) => {
|
||||
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
|
||||
app.get('/api/revised-files/:fileId/info', requireAuth, async (req, res) => {
|
||||
try {
|
||||
@@ -1216,200 +1427,67 @@ app.get('/api/revised-files/:fileId/info', requireAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ChatGPT integration routes
|
||||
app.get('/chat', requireAuth, (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) => {
|
||||
// Render revised notes content endpoint
|
||||
app.post('/api/render-revised-content', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const { message } = req.body;
|
||||
const { content, displayMode = 'markdown', autoDetect = true } = req.body;
|
||||
|
||||
// Initialize chat history in session if it doesn't exist
|
||||
if (!req.session.chatHistory) {
|
||||
req.session.chatHistory = [];
|
||||
if (!content) {
|
||||
return res.json({
|
||||
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({
|
||||
success: true,
|
||||
history: chatHistory
|
||||
renderedContent: renderedContent,
|
||||
displayMode: displayMode,
|
||||
detectedFormat: detectedFormat,
|
||||
isMarkdownContent: isMarkdownContent
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error getting chat history:', error);
|
||||
console.error('Error rendering content:', error);
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Failed to get chat history',
|
||||
error: 'Failed to render content',
|
||||
details: error.message
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Clear chat history
|
||||
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
|
||||
// Get file preview endpoint (simplified for dashboard)
|
||||
app.get('/api/files/:fileId/preview', requireAuth, async (req, res) => {
|
||||
try {
|
||||
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);
|
||||
|
||||
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 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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
res.json({
|
||||
return res.json({
|
||||
success: true,
|
||||
file: {
|
||||
id: file.id,
|
||||
@@ -1457,7 +1625,6 @@ app.get('/api/files/:fileId/preview', requireAuth, async (req, res) => {
|
||||
} else {
|
||||
// Failed to extract text, fall back to file type detection
|
||||
const textFormats = ['.txt', '.md', '.json', '.js', '.html', '.css', '.xml', '.csv'];
|
||||
const binaryFormats = ['.pdf', '.docx', '.doc', '.xlsx', '.xls', '.pptx', '.ppt'];
|
||||
|
||||
if (textFormats.includes(fileExtension)) {
|
||||
// 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;
|
||||
|
||||
res.json({
|
||||
return res.json({
|
||||
success: true,
|
||||
file: {
|
||||
id: file.id,
|
||||
@@ -1479,7 +1646,7 @@ app.get('/api/files/:fileId/preview', requireAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
} catch (readError) {
|
||||
res.json({
|
||||
return res.json({
|
||||
success: true,
|
||||
file: {
|
||||
id: file.id,
|
||||
@@ -1494,14 +1661,213 @@ app.get('/api/files/:fileId/preview', requireAuth, async (req, res) => {
|
||||
}
|
||||
} else {
|
||||
// Binary format that couldn't be processed
|
||||
res.json({
|
||||
return res.json({
|
||||
success: true,
|
||||
file: {
|
||||
id: file.id,
|
||||
originalName: file.originalName,
|
||||
size: file.size,
|
||||
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',
|
||||
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}`);
|
||||
});
|
||||
|
||||
// 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
|
||||
• 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?`;
|
||||
const formattedWelcome = formatMessage(welcomeText, true);
|
||||
|
||||
|
||||
@@ -890,25 +890,100 @@ async function previewRevisedFile(fileId) {
|
||||
|
||||
const file = fileInfo.file;
|
||||
|
||||
// Download the file content
|
||||
const contentResponse = await fetch(`/uploads/revised-notes/${file.filename}`);
|
||||
// Get file content using the new API endpoint
|
||||
const contentResponse = await fetch(`/api/revised-files/${fileId}/content`);
|
||||
if (!contentResponse.ok) {
|
||||
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'));
|
||||
|
||||
// Create preview content with display mode toggle
|
||||
document.getElementById('preview-content').innerHTML = `
|
||||
<div class="alert alert-info">
|
||||
<i class="fas fa-brain me-2"></i>
|
||||
<strong>AI-Revised Content</strong> • ${file.revisionType} • From: ${file.originalFileName || 'Unknown'}
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
<div class="alert alert-info mb-0 flex-grow-1 me-3">
|
||||
<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 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>
|
||||
<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; margin: 0;">${escapeHtml(content)}</pre>
|
||||
</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();
|
||||
} catch (error) {
|
||||
console.error('Error previewing revised file:', error);
|
||||
|
||||
@@ -35,7 +35,20 @@
|
||||
</div>
|
||||
</div>
|
||||
<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;">
|
||||
<p class="text-muted text-center mt-5">Select a revision type and click "Revise" to see AI-enhanced notes here.</p>
|
||||
</div>
|
||||
@@ -139,11 +152,72 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const revisionType = document.getElementById('revision-type');
|
||||
const saveBtn = document.getElementById('save-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 content = <%- JSON.stringify(content) %>;
|
||||
let currentRevisedContent = '';
|
||||
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() {
|
||||
const type = revisionType.value;
|
||||
@@ -153,6 +227,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
reviseBtn.disabled = true;
|
||||
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>';
|
||||
|
||||
try {
|
||||
@@ -175,9 +250,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log('Revision result:', result);
|
||||
|
||||
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;
|
||||
currentRevisionType = type;
|
||||
|
||||
// Show display mode toggle
|
||||
displayModeToggle.style.display = 'block';
|
||||
|
||||
// Render content based on current display mode
|
||||
await renderRevisedContent(currentRevisedContent, currentDisplayMode);
|
||||
|
||||
saveBtn.disabled = false;
|
||||
downloadBtn.disabled = false;
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user