diff --git a/package-lock.json b/package-lock.json
index 1b02546..0fdc099 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 00b96f4..e79ba1c 100644
--- a/package.json
+++ b/package.json
@@ -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"
diff --git a/public/css/style.css b/public/css/style.css
index bf733b4..4edc7e3 100644
--- a/public/css/style.css
+++ b/public/css/style.css
@@ -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;
}
}
diff --git a/server.js b/server.js
index c0c006e..8cc2f07 100644
--- a/server.js
+++ b/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, '
');
+ }
+ 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
+ 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);
+
diff --git a/views/chat.ejs b/views/chat.ejs
index 9c4e1e2..a9eed54 100644
--- a/views/chat.ejs
+++ b/views/chat.ejs
@@ -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);
diff --git a/views/dashboard.ejs b/views/dashboard.ejs
index adc90e9..64182d6 100644
--- a/views/dashboard.ejs
+++ b/views/dashboard.ejs
@@ -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 = `
-
${escapeHtml(content)}
+ ${escapeHtml(content)}
${escapeHtml(renderResult.renderedContent)}`;
+ 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 = `${escapeHtml(content)}`;
+ contentContainer.style.backgroundColor = '#f8f9fa';
+ }
+ }
+
+ modeMarkdown.addEventListener('change', updatePreviewMode);
+ modeHtml.addEventListener('change', updatePreviewMode);
+
modal.show();
} catch (error) {
console.error('Error previewing revised file:', error);
diff --git a/views/revise.ejs b/views/revise.ejs
index a6c89d0..8cf67b5 100644
--- a/views/revise.ejs
+++ b/views/revise.ejs
@@ -35,7 +35,20 @@
Select a revision type and click "Revise" to see AI-enhanced notes here.
${escapeHtml(result.renderedContent)}`;
+ }
+ } else {
+ throw new Error(result.error || 'Failed to render content');
+ }
+ } catch (error) {
+ console.error('Error rendering content:', error);
+ // Fallback to escaped text
+ revisedContent.innerHTML = `${escapeHtml(content)}`;
+ }
+ }
+
+ // 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 = 'AI is processing your notes...
' + escapeHtml(result.revisedContent) + ''; 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 {