Add HTML & Markdown preview in dashboard and revise pages

This commit is contained in:
inubimambo
2025-07-12 13:53:07 +08:00
parent 51c3c6b577
commit a08f767841
7 changed files with 1653 additions and 207 deletions

575
package-lock.json generated
View File

@@ -14,6 +14,7 @@
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"connect-flash": "^0.1.1", "connect-flash": "^0.1.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"dompurify": "^3.2.6",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"ejs": "^3.1.10", "ejs": "^3.1.10",
"exceljs": "^4.4.0", "exceljs": "^4.4.0",
@@ -21,7 +22,9 @@
"express-session": "^1.18.0", "express-session": "^1.18.0",
"form-data": "^4.0.3", "form-data": "^4.0.3",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"jsdom": "^26.1.0",
"mammoth": "^1.9.1", "mammoth": "^1.9.1",
"marked": "^16.0.0",
"multer": "^2.0.0", "multer": "^2.0.0",
"pdf-parse": "^1.1.1", "pdf-parse": "^1.1.1",
"uuid": "^10.0.0" "uuid": "^10.0.0"
@@ -30,6 +33,129 @@
"nodemon": "^3.0.1" "nodemon": "^3.0.1"
} }
}, },
"node_modules/@asamuzakjp/css-color": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
"integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
"license": "MIT",
"dependencies": {
"@csstools/css-calc": "^2.1.3",
"@csstools/css-color-parser": "^3.0.9",
"@csstools/css-parser-algorithms": "^3.0.4",
"@csstools/css-tokenizer": "^3.0.3",
"lru-cache": "^10.4.3"
}
},
"node_modules/@csstools/color-helpers": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.2.tgz",
"integrity": "sha512-JqWH1vsgdGcw2RR6VliXXdA0/59LttzlU8UlRT/iUUsEeWfYq8I+K0yhihEUTTHLRm1EXvpsCx3083EU15ecsA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT-0",
"engines": {
"node": ">=18"
}
},
"node_modules/@csstools/css-calc": {
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
"integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^3.0.5",
"@csstools/css-tokenizer": "^3.0.4"
}
},
"node_modules/@csstools/css-color-parser": {
"version": "3.0.10",
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.10.tgz",
"integrity": "sha512-TiJ5Ajr6WRd1r8HSiwJvZBiJOqtH86aHpUjq5aEKWHiII2Qfjqd/HCWKPOW8EP4vcspXbHnXrwIDlu5savQipg==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"dependencies": {
"@csstools/color-helpers": "^5.0.2",
"@csstools/css-calc": "^2.1.4"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-parser-algorithms": "^3.0.5",
"@csstools/css-tokenizer": "^3.0.4"
}
},
"node_modules/@csstools/css-parser-algorithms": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
"integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=18"
},
"peerDependencies": {
"@csstools/css-tokenizer": "^3.0.4"
}
},
"node_modules/@csstools/css-tokenizer": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
"integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/csstools"
},
{
"type": "opencollective",
"url": "https://opencollective.com/csstools"
}
],
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@fast-csv/format": { "node_modules/@fast-csv/format": {
"version": "4.3.5", "version": "4.3.5",
"resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz", "resolved": "https://registry.npmjs.org/@fast-csv/format/-/format-4.3.5.tgz",
@@ -85,6 +211,13 @@
"integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==", "integrity": "sha512-fAtCfv4jJg+ExtXhvCkCqUKZ+4ok/JQk01qDKhL5BDDoS3AxKXhV5/MAVUZyQnSEd2GT92fkgZl0pz0Q0AzcIQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
"integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
"license": "MIT",
"optional": true
},
"node_modules/@xmldom/xmldom": { "node_modules/@xmldom/xmldom": {
"version": "0.8.10", "version": "0.8.10",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz", "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.10.tgz",
@@ -794,6 +927,66 @@
"node": ">= 10" "node": ">= 10"
} }
}, },
"node_modules/cssstyle": {
"version": "4.6.0",
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
"integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
"license": "MIT",
"dependencies": {
"@asamuzakjp/css-color": "^3.2.0",
"rrweb-cssom": "^0.8.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/data-urls": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
"integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
"license": "MIT",
"dependencies": {
"whatwg-mimetype": "^4.0.0",
"whatwg-url": "^14.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/data-urls/node_modules/tr46": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/data-urls/node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/data-urls/node_modules/whatwg-url": {
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
"license": "MIT",
"dependencies": {
"tr46": "^5.1.0",
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/dayjs": { "node_modules/dayjs": {
"version": "1.11.13", "version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
@@ -809,6 +1002,12 @@
"ms": "2.0.0" "ms": "2.0.0"
} }
}, },
"node_modules/decimal.js": {
"version": "10.6.0",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
"license": "MIT"
},
"node_modules/delayed-stream": { "node_modules/delayed-stream": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
@@ -858,6 +1057,15 @@
"integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==", "integrity": "sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==",
"license": "BSD-2-Clause" "license": "BSD-2-Clause"
}, },
"node_modules/dompurify": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",
"integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/dotenv": { "node_modules/dotenv": {
"version": "16.6.1", "version": "16.6.1",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
@@ -977,6 +1185,18 @@
"once": "^1.4.0" "once": "^1.4.0"
} }
}, },
"node_modules/entities": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/es-define-property": { "node_modules/es-define-property": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -1541,6 +1761,18 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/html-encoding-sniffer": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
"integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
"license": "MIT",
"dependencies": {
"whatwg-encoding": "^3.1.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/http-errors": { "node_modules/http-errors": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@@ -1557,6 +1789,51 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/http-proxy-agent": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.0",
"debug": "^4.3.4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/http-proxy-agent/node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/http-proxy-agent/node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/http-proxy-agent/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/https-proxy-agent": { "node_modules/https-proxy-agent": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
@@ -1719,6 +1996,12 @@
"node": ">=0.12.0" "node": ">=0.12.0"
} }
}, },
"node_modules/is-potential-custom-element-name": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
"license": "MIT"
},
"node_modules/isarray": { "node_modules/isarray": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
@@ -1743,6 +2026,136 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/jsdom": {
"version": "26.1.0",
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz",
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
"license": "MIT",
"dependencies": {
"cssstyle": "^4.2.1",
"data-urls": "^5.0.0",
"decimal.js": "^10.5.0",
"html-encoding-sniffer": "^4.0.0",
"http-proxy-agent": "^7.0.2",
"https-proxy-agent": "^7.0.6",
"is-potential-custom-element-name": "^1.0.1",
"nwsapi": "^2.2.16",
"parse5": "^7.2.1",
"rrweb-cssom": "^0.8.0",
"saxes": "^6.0.0",
"symbol-tree": "^3.2.4",
"tough-cookie": "^5.1.1",
"w3c-xmlserializer": "^5.0.0",
"webidl-conversions": "^7.0.0",
"whatwg-encoding": "^3.1.1",
"whatwg-mimetype": "^4.0.0",
"whatwg-url": "^14.1.1",
"ws": "^8.18.0",
"xml-name-validator": "^5.0.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"canvas": "^3.0.0"
},
"peerDependenciesMeta": {
"canvas": {
"optional": true
}
}
},
"node_modules/jsdom/node_modules/agent-base": {
"version": "7.1.4",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
"license": "MIT",
"engines": {
"node": ">= 14"
}
},
"node_modules/jsdom/node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/jsdom/node_modules/https-proxy-agent": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
"license": "MIT",
"dependencies": {
"agent-base": "^7.1.2",
"debug": "4"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/jsdom/node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/jsdom/node_modules/saxes": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
"license": "ISC",
"dependencies": {
"xmlchars": "^2.2.0"
},
"engines": {
"node": ">=v12.22.7"
}
},
"node_modules/jsdom/node_modules/tr46": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/jsdom/node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/jsdom/node_modules/whatwg-url": {
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
"license": "MIT",
"dependencies": {
"tr46": "^5.1.0",
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/jsonfile": { "node_modules/jsonfile": {
"version": "6.1.0", "version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
@@ -1944,6 +2357,12 @@
"underscore": "^1.13.1" "underscore": "^1.13.1"
} }
}, },
"node_modules/lru-cache": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
"license": "ISC"
},
"node_modules/make-dir": { "node_modules/make-dir": {
"version": "3.1.0", "version": "3.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
@@ -1992,6 +2411,18 @@
"node": ">=12.0.0" "node": ">=12.0.0"
} }
}, },
"node_modules/marked": {
"version": "16.0.0",
"resolved": "https://registry.npmjs.org/marked/-/marked-16.0.0.tgz",
"integrity": "sha512-MUKMXDjsD/eptB7GPzxo4xcnLS6oo7/RHimUMHEDRhUooPwmN9BEpMl7AEOJv3bmso169wHI2wUF9VQgL7zfmA==",
"license": "MIT",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 20"
}
},
"node_modules/math-intrinsics": { "node_modules/math-intrinsics": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -2307,6 +2738,12 @@
"set-blocking": "^2.0.0" "set-blocking": "^2.0.0"
} }
}, },
"node_modules/nwsapi": {
"version": "2.2.20",
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.20.tgz",
"integrity": "sha512-/ieB+mDe4MrrKMT8z+mQL8klXydZWGR5Dowt4RAGKbJ3kIGEx3X4ljUo+6V73IXtUPWgfOlU5B9MlGxFO5T+cA==",
"license": "MIT"
},
"node_modules/object-assign": { "node_modules/object-assign": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -2370,6 +2807,18 @@
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)" "license": "(MIT AND Zlib)"
}, },
"node_modules/parse5": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
"license": "MIT",
"dependencies": {
"entities": "^6.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
"node_modules/parseurl": { "node_modules/parseurl": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -2467,6 +2916,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/qs": { "node_modules/qs": {
"version": "6.13.0", "version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
@@ -2588,6 +3046,12 @@
"url": "https://github.com/sponsors/isaacs" "url": "https://github.com/sponsors/isaacs"
} }
}, },
"node_modules/rrweb-cssom": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
"license": "MIT"
},
"node_modules/safe-buffer": { "node_modules/safe-buffer": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -2871,6 +3335,12 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
"license": "MIT"
},
"node_modules/tar": { "node_modules/tar": {
"version": "6.2.1", "version": "6.2.1",
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
@@ -2916,6 +3386,24 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/tldts": {
"version": "6.1.86",
"resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz",
"integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
"license": "MIT",
"dependencies": {
"tldts-core": "^6.1.86"
},
"bin": {
"tldts": "bin/cli.js"
}
},
"node_modules/tldts-core": {
"version": "6.1.86",
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
"integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
"license": "MIT"
},
"node_modules/tmp": { "node_modules/tmp": {
"version": "0.2.3", "version": "0.2.3",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz",
@@ -2957,6 +3445,18 @@
"nodetouch": "bin/nodetouch.js" "nodetouch": "bin/nodetouch.js"
} }
}, },
"node_modules/tough-cookie": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz",
"integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
"license": "BSD-3-Clause",
"dependencies": {
"tldts": "^6.1.32"
},
"engines": {
"node": ">=16"
}
},
"node_modules/tr46": { "node_modules/tr46": {
"version": "0.0.3", "version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
@@ -3119,12 +3619,57 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/w3c-xmlserializer": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
"license": "MIT",
"dependencies": {
"xml-name-validator": "^5.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/webidl-conversions": { "node_modules/webidl-conversions": {
"version": "3.0.1", "version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"license": "BSD-2-Clause" "license": "BSD-2-Clause"
}, },
"node_modules/whatwg-encoding": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
"license": "MIT",
"dependencies": {
"iconv-lite": "0.6.3"
},
"engines": {
"node": ">=18"
}
},
"node_modules/whatwg-encoding/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/whatwg-mimetype": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/whatwg-url": { "node_modules/whatwg-url": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
@@ -3150,6 +3695,36 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/ws": {
"version": "8.18.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
"integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xml-name-validator": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
"license": "Apache-2.0",
"engines": {
"node": ">=18"
}
},
"node_modules/xmlbuilder": { "node_modules/xmlbuilder": {
"version": "10.1.1", "version": "10.1.1",
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-10.1.1.tgz",

View File

@@ -13,6 +13,7 @@
"body-parser": "^1.20.2", "body-parser": "^1.20.2",
"connect-flash": "^0.1.1", "connect-flash": "^0.1.1",
"cors": "^2.8.5", "cors": "^2.8.5",
"dompurify": "^3.2.6",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"ejs": "^3.1.10", "ejs": "^3.1.10",
"exceljs": "^4.4.0", "exceljs": "^4.4.0",
@@ -20,7 +21,9 @@
"express-session": "^1.18.0", "express-session": "^1.18.0",
"form-data": "^4.0.3", "form-data": "^4.0.3",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"jsdom": "^26.1.0",
"mammoth": "^1.9.1", "mammoth": "^1.9.1",
"marked": "^16.0.0",
"multer": "^2.0.0", "multer": "^2.0.0",
"pdf-parse": "^1.1.1", "pdf-parse": "^1.1.1",
"uuid": "^10.0.0" "uuid": "^10.0.0"

View File

@@ -740,20 +740,231 @@ body {
border-color: rgba(255,255,255,0.3); border-color: rgba(255,255,255,0.3);
} }
/* Responsive adjustments for mobile */ /* Rendered Markdown Styles */
.rendered-markdown {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: #333;
max-width: 100%;
word-wrap: break-word;
}
.rendered-markdown h1,
.rendered-markdown h2,
.rendered-markdown h3,
.rendered-markdown h4,
.rendered-markdown h5,
.rendered-markdown h6 {
margin-top: 1.5rem;
margin-bottom: 0.75rem;
font-weight: 600;
line-height: 1.25;
}
.rendered-markdown h1 {
font-size: 1.75rem;
border-bottom: 2px solid #e1e4e8;
padding-bottom: 0.5rem;
}
.rendered-markdown h2 {
font-size: 1.5rem;
border-bottom: 1px solid #e1e4e8;
padding-bottom: 0.3rem;
}
.rendered-markdown h3 {
font-size: 1.25rem;
}
.rendered-markdown h4 {
font-size: 1.1rem;
}
.rendered-markdown h5,
.rendered-markdown h6 {
font-size: 1rem;
}
.rendered-markdown p {
margin-bottom: 1rem;
}
.rendered-markdown strong,
.rendered-markdown b {
font-weight: 600;
}
.rendered-markdown em,
.rendered-markdown i {
font-style: italic;
}
.rendered-markdown ul,
.rendered-markdown ol {
margin-bottom: 1rem;
padding-left: 2rem;
}
.rendered-markdown li {
margin-bottom: 0.25rem;
}
.rendered-markdown li > p {
margin-bottom: 0.5rem;
}
.rendered-markdown ul {
list-style-type: disc;
}
.rendered-markdown ol {
list-style-type: decimal;
}
.rendered-markdown blockquote {
margin: 1rem 0;
padding: 0.5rem 1rem;
border-left: 4px solid #dfe2e5;
background-color: #f6f8fa;
color: #6a737d;
}
.rendered-markdown blockquote p:last-child {
margin-bottom: 0;
}
.rendered-markdown pre {
background-color: #f6f8fa;
border-radius: 6px;
font-size: 0.875rem;
line-height: 1.45;
overflow: auto;
padding: 1rem;
margin-bottom: 1rem;
font-family: 'Courier New', Consolas, monospace;
}
.rendered-markdown code {
background-color: rgba(27, 31, 35, 0.05);
border-radius: 3px;
font-size: 0.875rem;
margin: 0;
padding: 0.2em 0.4em;
font-family: 'Courier New', Consolas, monospace;
}
.rendered-markdown pre code {
background-color: transparent;
border-radius: 0;
font-size: inherit;
margin: 0;
padding: 0;
word-break: normal;
white-space: pre;
word-wrap: normal;
}
.rendered-markdown table {
border-collapse: collapse;
margin-bottom: 1rem;
width: 100%;
}
.rendered-markdown table th,
.rendered-markdown table td {
border: 1px solid #dfe2e5;
padding: 6px 13px;
text-align: left;
}
.rendered-markdown table th {
background-color: #f6f8fa;
font-weight: 600;
}
.rendered-markdown table tr:nth-child(2n) {
background-color: #f6f8fa;
}
.rendered-markdown a {
color: #0366d6;
text-decoration: none;
}
.rendered-markdown a:hover {
text-decoration: underline;
}
.rendered-markdown hr {
border: none;
border-top: 1px solid #e1e4e8;
height: 1px;
margin: 1.5rem 0;
}
.rendered-markdown img {
max-width: 100%;
height: auto;
border-radius: 6px;
margin: 0.5rem 0;
}
/* Display mode toggle styling */
#display-mode-toggle .btn-outline-secondary {
border-color: #6c757d;
color: #6c757d;
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
#display-mode-toggle .btn-check:checked + .btn-outline-secondary {
background-color: #6c757d;
border-color: #6c757d;
color: white;
}
#display-mode-toggle .btn-outline-secondary:hover {
background-color: #5c636a;
border-color: #565e64;
color: white;
}
/* Alert for format detection */
.alert-sm {
padding: 0.375rem 0.75rem;
font-size: 0.875rem;
}
/* Responsive adjustments */
@media (max-width: 768px) { @media (max-width: 768px) {
.message-text .code-block { .rendered-markdown {
font-size: 0.9rem;
}
.rendered-markdown h1 {
font-size: 1.5rem;
}
.rendered-markdown h2 {
font-size: 1.3rem;
}
.rendered-markdown h3 {
font-size: 1.1rem;
}
.rendered-markdown pre {
padding: 0.75rem;
font-size: 0.8rem; font-size: 0.8rem;
padding: 0.5rem;
} }
.message-text .formatted-list { .rendered-markdown table {
padding-left: 1.2rem; font-size: 0.875rem;
} }
.message-text h3, .rendered-markdown ul,
.message-text h4, .rendered-markdown ol {
.message-text h5 { padding-left: 1.5rem;
font-size: 0.95rem;
} }
} }

871
server.js
View File

@@ -17,6 +17,15 @@ const mammoth = require('mammoth'); // For .docx files
const pdfParse = require('pdf-parse'); // For PDF files const pdfParse = require('pdf-parse'); // For PDF files
const ExcelJS = require('exceljs'); // For Excel files const ExcelJS = require('exceljs'); // For Excel files
// Markdown and HTML processing
const { marked } = require('marked');
const createDOMPurify = require('dompurify');
const { JSDOM } = require('jsdom');
// Initialize DOMPurify
const window = new JSDOM('').window;
const DOMPurify = createDOMPurify(window);
// Helper function to extract text from various document formats // Helper function to extract text from various document formats
async function extractTextFromDocument(filePath, fileExtension) { async function extractTextFromDocument(filePath, fileExtension) {
try { try {
@@ -705,6 +714,82 @@ app.get('/logout', (req, res) => {
}); });
}); });
// Dashboard route
app.get('/dashboard', requireAuth, async (req, res) => {
try {
// Load user files from both session and persistent storage
let allFiles = [];
let revisedFiles = [];
// Load persistent files
try {
const persistentFiles = await loadUserFiles(req.session.userId);
const persistentRevisedFiles = await loadRevisedFiles(req.session.userId);
allFiles = persistentFiles;
revisedFiles = persistentRevisedFiles;
// Merge with session data (session data is more current)
const sessionFiles = req.session.uploadedFiles || [];
const sessionRevisedFiles = req.session.revisedFiles || [];
// Update session with persistent data if session is empty
if (sessionFiles.length === 0 && persistentFiles.length > 0) {
req.session.uploadedFiles = persistentFiles;
}
if (sessionRevisedFiles.length === 0 && persistentRevisedFiles.length > 0) {
req.session.revisedFiles = persistentRevisedFiles;
}
// Use session data as the source of truth for display
allFiles = req.session.uploadedFiles || persistentFiles;
revisedFiles = req.session.revisedFiles || persistentRevisedFiles;
} catch (error) {
console.error('Error loading user files for dashboard:', error);
// Fall back to session data
allFiles = req.session.uploadedFiles || [];
revisedFiles = req.session.revisedFiles || [];
}
res.render('dashboard', {
title: 'Dashboard - EduCat',
files: allFiles,
revisedFiles: revisedFiles
});
} catch (error) {
console.error('Dashboard error:', error);
req.flash('error', 'Error loading dashboard');
res.redirect('/');
}
});
// Chat route
app.get('/chat', requireAuth, async (req, res) => {
try {
// Load user chat history
let chatHistory = [];
try {
chatHistory = await loadChatHistory(req.session.userId);
} catch (error) {
console.error('Error loading chat history:', error);
chatHistory = [];
}
res.render('chat', {
title: 'AI Chat - EduCat',
chatHistory: chatHistory
});
} catch (error) {
console.error('Chat route error:', error);
res.render('chat', {
title: 'AI Chat - EduCat',
chatHistory: []
});
}
});
app.get('/upload', requireAuth, (req, res) => { app.get('/upload', requireAuth, (req, res) => {
res.render('upload', { res.render('upload', {
title: 'Upload Your Notes - EduCat' title: 'Upload Your Notes - EduCat'
@@ -995,6 +1080,82 @@ app.post('/api/revise', requireAuth, async (req, res) => {
} }
}); });
// Chat API endpoint
app.post('/api/chat', requireAuth, async (req, res) => {
try {
const { message, history = [] } = req.body;
if (!message || message.trim().length === 0) {
return res.json({
success: false,
error: 'Message is required'
});
}
// Load existing chat history from storage
let existingHistory = [];
try {
existingHistory = await loadChatHistory(req.session.userId);
} catch (error) {
console.log('No existing chat history found, starting fresh');
}
// Prepare history for API call (last 10 conversations)
const recentHistory = existingHistory.slice(-10).map(conv => [
{ role: 'human', content: conv.human },
{ role: 'ai', content: conv.ai }
]).flat();
// Call Flowise API for chat
const response = await axios.post(`${FLOWISE_API_URL}/${FLOWISE_CHATFLOW_ID}`, {
question: message.trim(),
history: recentHistory
});
const botResponse = response.data.text || response.data.answer || 'Sorry, I could not process your request.';
// Save the conversation to history
const conversation = {
human: message.trim(),
ai: botResponse,
timestamp: new Date().toISOString()
};
existingHistory.push(conversation);
await saveChatHistory(req.session.userId, existingHistory);
res.json({
success: true,
response: botResponse
});
} catch (error) {
console.error('Chat error:', error);
res.json({
success: false,
error: 'Failed to get response from AI. Please try again.',
details: error.message
});
}
});
// Delete chat history endpoint
app.delete('/api/chat/history', requireAuth, async (req, res) => {
try {
await clearChatHistory(req.session.userId);
res.json({
success: true,
message: 'Chat history cleared successfully'
});
} catch (error) {
console.error('Error clearing chat history:', error);
res.json({
success: false,
error: 'Failed to clear chat history',
details: error.message
});
}
});
// Save revised notes endpoint // Save revised notes endpoint
app.post('/api/save-revised', requireAuth, async (req, res) => { app.post('/api/save-revised', requireAuth, async (req, res) => {
try { try {
@@ -1177,6 +1338,56 @@ app.delete('/api/revised-files/:fileId', requireAuth, async (req, res) => {
} }
}); });
// Get revised file content with rendering options
app.get('/api/revised-files/:fileId/content', requireAuth, async (req, res) => {
try {
const fileId = req.params.fileId;
const { displayMode = 'markdown' } = req.query;
const revisedFiles = req.session.revisedFiles || [];
const file = revisedFiles.find(f => f.id === fileId);
if (!file) {
// Try to load from persistent storage
const persistentRevisedFiles = await loadRevisedFiles(req.session.userId);
const persistentFile = persistentRevisedFiles.find(f => f.id === fileId);
if (!persistentFile) {
return res.status(404).json({
success: false,
error: 'File not found'
});
}
// Read file content
const content = await fs.readFile(persistentFile.path, 'utf8');
return res.json({
success: true,
file: persistentFile,
content: content,
displayMode: displayMode
});
}
// Read file content
const content = await fs.readFile(file.path, 'utf8');
res.json({
success: true,
file: file,
content: content,
displayMode: displayMode
});
} catch (error) {
console.error('Error getting revised file content:', error);
res.status(500).json({
success: false,
error: 'Failed to get file content',
details: error.message
});
}
});
// Get revised file info endpoint // Get revised file info endpoint
app.get('/api/revised-files/:fileId/info', requireAuth, async (req, res) => { app.get('/api/revised-files/:fileId/info', requireAuth, async (req, res) => {
try { try {
@@ -1216,200 +1427,67 @@ app.get('/api/revised-files/:fileId/info', requireAuth, async (req, res) => {
} }
}); });
// ChatGPT integration routes // Render revised notes content endpoint
app.get('/chat', requireAuth, (req, res) => { app.post('/api/render-revised-content', requireAuth, async (req, res) => {
// Initialize chat history if it doesn't exist
if (!req.session.chatHistory) {
req.session.chatHistory = [];
}
// Initialize chat session ID if it doesn't exist
if (!req.session.chatSessionId) {
req.session.chatSessionId = `educat-${req.session.userId}-${Date.now()}`;
}
res.render('chat', {
title: 'Chat with EduCat AI',
chatHistory: req.session.chatHistory
});
});
app.post('/api/chat', requireAuth, async (req, res) => {
try { try {
const { message } = req.body; const { content, displayMode = 'markdown', autoDetect = true } = req.body;
// Initialize chat history in session if it doesn't exist if (!content) {
if (!req.session.chatHistory) { return res.json({
req.session.chatHistory = []; success: false,
error: 'No content provided'
});
} }
// Initialize or get persistent chat session ID for this user
if (!req.session.chatSessionId) {
req.session.chatSessionId = `${req.session.userId}-${Date.now()}`;
let renderedContent = '';
let detectedFormat = 'text';
let isMarkdownContent = false;
// Auto-detect if content is markdown (if autoDetect is enabled)
if (autoDetect) {
isMarkdownContent = isLikelyMarkdown(content);
detectedFormat = isMarkdownContent ? 'markdown' : 'text';
} }
// Prepare the request payload for Flowise with sessionId and chatId
const flowisePayload = {
question: message,
sessionId: req.session.chatSessionId
};
// Add chatId if we have one from previous conversations
if (req.session.chatId) {
flowisePayload.chatId = req.session.chatId;
// Process content based on display mode
switch (displayMode) {
case 'html':
if (isMarkdownContent || autoDetect === false) {
// Convert markdown to safe HTML
renderedContent = markdownToSafeHtml(content);
} else {
// Just escape HTML and preserve line breaks for plain text
renderedContent = escapeHtml(content).replace(/\n/g, '<br>');
}
break;
case 'markdown':
case 'raw':
default:
// Return raw content (for markdown view or plain text)
renderedContent = content;
break;
} }
// Call Flowise API for chat with session history and sessionId
const response = await axios.post(`${FLOWISE_API_URL}/${FLOWISE_CHATFLOW_ID}`, flowisePayload);
const aiResponse = response.data.text || response.data.answer || 'No response received';
// Save the chatId from Flowise response for future requests
if (response.data.chatId) {
req.session.chatId = response.data.chatId;
}
// Add the conversation to session history
req.session.chatHistory.push({
human: message,
ai: aiResponse
});
// Save session explicitly since we modified it
req.session.save((err) => {
if (err) {
console.error('Error saving chat session:', err);
}
});
res.json({
success: true,
response: aiResponse
});
} catch (error) {
console.error('Chat error:', error);
res.status(500).json({
success: false,
error: 'Failed to get chat response',
details: error.message
});
}
});
// Get chat history from session
app.get('/api/chat/history', requireAuth, (req, res) => {
try {
const chatHistory = req.session.chatHistory || [];
res.json({ res.json({
success: true, success: true,
history: chatHistory renderedContent: renderedContent,
displayMode: displayMode,
detectedFormat: detectedFormat,
isMarkdownContent: isMarkdownContent
}); });
} catch (error) { } catch (error) {
console.error('Error getting chat history:', error); console.error('Error rendering content:', error);
res.status(500).json({ res.status(500).json({
success: false, success: false,
error: 'Failed to get chat history', error: 'Failed to render content',
details: error.message details: error.message
}); });
} }
}); });
// Clear chat history // Get file preview endpoint (simplified for dashboard)
app.delete('/api/chat/history', requireAuth, (req, res) => {
try {
req.session.chatHistory = [];
// Reset the session ID to start a fresh conversation
req.session.chatSessionId = `${req.session.userId}-${Date.now()}`;
// Clear the Flowise chatId
delete req.session.chatId;
req.session.save((err) => {
if (err) {
console.error('Error clearing chat session:', err);
return res.status(500).json({
success: false,
error: 'Failed to clear chat history'
});
}
res.json({
success: true,
message: 'Chat history cleared'
});
});
} catch (error) {
console.error('Error clearing chat history:', error);
res.status(500).json({
success: false,
error: 'Failed to clear chat history',
details: error.message
});
}
});
app.get('/dashboard', requireAuth, async (req, res) => {
try {
// Load persistent files for this user
const persistentFiles = await loadUserFiles(req.session.userId);
// Load revised files separately
const revisedFiles = await loadRevisedFiles(req.session.userId);
// Merge with session files (in case there are newly uploaded files not yet saved)
const sessionFiles = req.session.uploadedFiles || [];
const allFiles = [...persistentFiles];
// Add any session files that aren't already in persistent storage
sessionFiles.forEach(sessionFile => {
if (!persistentFiles.find(f => f.id === sessionFile.id)) {
allFiles.push(sessionFile);
}
});
// Merge revised files from session
const sessionRevisedFiles = req.session.revisedFiles || [];
const allRevisedFiles = [...revisedFiles];
sessionRevisedFiles.forEach(sessionFile => {
if (!revisedFiles.find(f => f.id === sessionFile.id)) {
allRevisedFiles.push(sessionFile);
}
});
// Update session with merged files for current session use
req.session.uploadedFiles = allFiles;
req.session.revisedFiles = allRevisedFiles;
res.render('dashboard', {
title: 'Dashboard - EduCat',
files: allFiles,
revisedFiles: allRevisedFiles
});
} catch (error) {
console.error('Error loading dashboard:', error);
res.render('dashboard', {
title: 'Dashboard - EduCat',
files: req.session.uploadedFiles || [],
revisedFiles: []
});
}
});
// File management endpoints
app.get('/api/files/:fileId/preview', requireAuth, async (req, res) => { app.get('/api/files/:fileId/preview', requireAuth, async (req, res) => {
try { try {
const fileId = req.params.fileId; const fileId = req.params.fileId;
@@ -1417,12 +1495,102 @@ app.get('/api/files/:fileId/preview', requireAuth, async (req, res) => {
const file = files.find(f => f.id === fileId); const file = files.find(f => f.id === fileId);
if (!file) { if (!file) {
return res.status(404).json({ success: false, error: 'File not found' }); // Try to load from persistent storage
try {
const persistentFiles = await loadUserFiles(req.session.userId);
const persistentFile = persistentFiles.find(f => f.id === fileId);
if (!persistentFile) {
return res.status(404).json({ success: false, error: 'File not found' });
}
// Process the persistent file
const filePath = path.join(__dirname, persistentFile.path);
const fileExtension = path.extname(persistentFile.originalName).toLowerCase();
// Check if file exists
if (!await fs.pathExists(filePath)) {
return res.json({
success: true,
file: {
id: persistentFile.id,
originalName: persistentFile.originalName,
size: persistentFile.size,
uploadDate: persistentFile.uploadDate,
content: 'File not found on disk',
previewType: 'error',
message: `File "${persistentFile.originalName}" was uploaded but the physical file is no longer available on disk. Please re-upload the file.`
}
});
}
// Try to extract text from the document
const extractionResult = await extractTextFromDocument(filePath, fileExtension);
if (extractionResult.success) {
const extractedText = extractionResult.text;
const previewContent = extractedText.length > 5000 ?
extractedText.substring(0, 5000) + '\n\n... (content truncated for preview)' :
extractedText;
return res.json({
success: true,
file: {
id: persistentFile.id,
originalName: persistentFile.originalName,
size: persistentFile.size,
uploadDate: persistentFile.uploadDate,
content: previewContent,
previewType: 'extracted-text',
extractionInfo: {
method: extractionResult.method,
totalLength: extractionResult.extractedLength,
pages: extractionResult.pages || null,
sheets: extractionResult.sheets || null,
truncated: extractedText.length > 5000
},
message: `Text successfully extracted from ${fileExtension.toUpperCase()} file. ${extractedText.length > 5000 ? 'Preview truncated to first 5000 characters.' : ''}`
}
});
} else {
return res.json({
success: true,
file: {
id: persistentFile.id,
originalName: persistentFile.originalName,
size: persistentFile.size,
uploadDate: persistentFile.uploadDate,
content: 'Preview not available for this file type. File content is available for AI processing.',
previewType: 'extraction-failed',
message: `Failed to extract text from ${fileExtension.toUpperCase()} file: ${extractionResult.error}. The file has been uploaded and may still be usable for AI processing.`
}
});
}
} catch (error) {
console.error('Error loading from persistent storage:', error);
return res.status(404).json({ success: false, error: 'File not found' });
}
} }
const filePath = path.join(__dirname, file.path); const filePath = path.join(__dirname, file.path);
const fileExtension = path.extname(file.originalName).toLowerCase(); const fileExtension = path.extname(file.originalName).toLowerCase();
// Check if file exists
if (!await fs.pathExists(filePath)) {
return res.json({
success: true,
file: {
id: file.id,
originalName: file.originalName,
size: file.size,
uploadDate: file.uploadDate,
content: 'File not found on disk',
previewType: 'error',
message: `File "${file.originalName}" was uploaded but the physical file is no longer available on disk. Please re-upload the file.`
}
});
}
// Try to extract text from the document // Try to extract text from the document
const extractionResult = await extractTextFromDocument(filePath, fileExtension); const extractionResult = await extractTextFromDocument(filePath, fileExtension);
@@ -1435,7 +1603,7 @@ app.get('/api/files/:fileId/preview', requireAuth, async (req, res) => {
extractedText.substring(0, 5000) + '\n\n... (content truncated for preview)' : extractedText.substring(0, 5000) + '\n\n... (content truncated for preview)' :
extractedText; extractedText;
res.json({ return res.json({
success: true, success: true,
file: { file: {
id: file.id, id: file.id,
@@ -1457,7 +1625,6 @@ app.get('/api/files/:fileId/preview', requireAuth, async (req, res) => {
} else { } else {
// Failed to extract text, fall back to file type detection // Failed to extract text, fall back to file type detection
const textFormats = ['.txt', '.md', '.json', '.js', '.html', '.css', '.xml', '.csv']; const textFormats = ['.txt', '.md', '.json', '.js', '.html', '.css', '.xml', '.csv'];
const binaryFormats = ['.pdf', '.docx', '.doc', '.xlsx', '.xls', '.pptx', '.ppt'];
if (textFormats.includes(fileExtension)) { if (textFormats.includes(fileExtension)) {
// Try reading as plain text (should have been handled by extraction, but fallback) // Try reading as plain text (should have been handled by extraction, but fallback)
@@ -1467,7 +1634,7 @@ app.get('/api/files/:fileId/preview', requireAuth, async (req, res) => {
fileContent.substring(0, 5000) + '\n\n... (truncated)' : fileContent.substring(0, 5000) + '\n\n... (truncated)' :
fileContent; fileContent;
res.json({ return res.json({
success: true, success: true,
file: { file: {
id: file.id, id: file.id,
@@ -1479,7 +1646,7 @@ app.get('/api/files/:fileId/preview', requireAuth, async (req, res) => {
} }
}); });
} catch (readError) { } catch (readError) {
res.json({ return res.json({
success: true, success: true,
file: { file: {
id: file.id, id: file.id,
@@ -1494,14 +1661,213 @@ app.get('/api/files/:fileId/preview', requireAuth, async (req, res) => {
} }
} else { } else {
// Binary format that couldn't be processed // Binary format that couldn't be processed
res.json({ return res.json({
success: true, success: true,
file: { file: {
id: file.id, id: file.id,
originalName: file.originalName, originalName: file.originalName,
size: file.size, size: file.size,
uploadDate: file.uploadDate, uploadDate: file.uploadDate,
content: 'Text extraction failed', content: 'Preview not available for this file type. File content is available for AI processing.',
previewType: 'extraction-failed',
message: `Failed to extract text from ${fileExtension.toUpperCase()} file: ${extractionResult.error}. The file has been uploaded and may still be usable for AI processing.`
}
});
}
}
} catch (error) {
console.error('Error getting file preview:', error);
res.status(500).json({
success: false,
error: 'Failed to get file preview',
details: error.message
});
}
});
// Get file preview content endpoint (for notes and revisions)
app.get('/api/files/:fileId/preview-content', requireAuth, async (req, res) => {
try {
const fileId = req.params.fileId;
const { displayMode = 'markdown' } = req.query;
const files = req.session.uploadedFiles || [];
const file = files.find(f => f.id === fileId);
if (!file) {
// Try to load from persistent storage
try {
const persistentFiles = await loadUserFiles(req.session.userId);
const persistentFile = persistentFiles.find(f => f.id === fileId);
if (!persistentFile) {
return res.status(404).json({ success: false, error: 'File not found' });
}
// Process the persistent file
const filePath = path.join(__dirname, persistentFile.path);
const fileExtension = path.extname(persistentFile.originalName).toLowerCase();
// Check if file exists
if (!await fs.pathExists(filePath)) {
return res.json({
success: true,
file: {
id: persistentFile.id,
originalName: persistentFile.originalName,
size: persistentFile.size,
uploadDate: persistentFile.uploadDate,
content: 'File not found on disk',
previewType: 'error',
message: `File "${persistentFile.originalName}" was uploaded but the physical file is no longer available on disk. Please re-upload the file.`
}
});
}
// Try to extract text from the document
const extractionResult = await extractTextFromDocument(filePath, fileExtension);
if (extractionResult.success) {
const extractedText = extractionResult.text;
const previewContent = extractedText.length > 5000 ?
extractedText.substring(0, 5000) + '\n\n... (content truncated for preview)' :
extractedText;
return res.json({
success: true,
file: {
id: persistentFile.id,
originalName: persistentFile.originalName,
size: persistentFile.size,
uploadDate: persistentFile.uploadDate,
content: previewContent,
previewType: 'extracted-text',
extractionInfo: {
method: extractionResult.method,
totalLength: extractionResult.extractedLength,
pages: extractionResult.pages || null,
sheets: extractionResult.sheets || null,
truncated: extractedText.length > 5000
},
message: `Text successfully extracted from ${fileExtension.toUpperCase()} file. ${extractedText.length > 5000 ? 'Preview truncated to first 5000 characters.' : ''}`
}
});
} else {
return res.json({
success: true,
file: {
id: persistentFile.id,
originalName: persistentFile.originalName,
size: persistentFile.size,
uploadDate: persistentFile.uploadDate,
content: 'Preview not available for this file type. File content is available for AI processing.',
previewType: 'extraction-failed',
message: `Failed to extract text from ${fileExtension.toUpperCase()} file: ${extractionResult.error}. The file has been uploaded and may still be usable for AI processing.`
}
});
}
} catch (error) {
console.error('Error loading from persistent storage:', error);
return res.status(404).json({ success: false, error: 'File not found' });
}
}
const filePath = path.join(__dirname, file.path);
const fileExtension = path.extname(file.originalName).toLowerCase();
// Check if file exists
if (!await fs.pathExists(filePath)) {
return res.json({
success: true,
file: {
id: file.id,
originalName: file.originalName,
size: file.size,
uploadDate: file.uploadDate,
content: 'File not found on disk',
previewType: 'error',
message: `File "${file.originalName}" was uploaded but the physical file is no longer available on disk. Please re-upload the file.`
}
});
}
// Try to extract text from the document
const extractionResult = await extractTextFromDocument(filePath, fileExtension);
if (extractionResult.success) {
// Successfully extracted text
const extractedText = extractionResult.text;
// Limit preview to first 5000 characters to avoid huge responses
const previewContent = extractedText.length > 5000 ?
extractedText.substring(0, 5000) + '\n\n... (content truncated for preview)' :
extractedText;
return res.json({
success: true,
file: {
id: file.id,
originalName: file.originalName,
size: file.size,
uploadDate: file.uploadDate,
content: previewContent,
previewType: 'extracted-text',
extractionInfo: {
method: extractionResult.method,
totalLength: extractionResult.extractedLength,
pages: extractionResult.pages || null,
sheets: extractionResult.sheets || null,
truncated: extractedText.length > 5000
},
message: `Text successfully extracted from ${fileExtension.toUpperCase()} file. ${extractedText.length > 5000 ? 'Preview truncated to first 5000 characters.' : ''}`
}
});
} else {
// Failed to extract text, fall back to file type detection
const textFormats = ['.txt', '.md', '.json', '.js', '.html', '.css', '.xml', '.csv'];
if (textFormats.includes(fileExtension)) {
// Try reading as plain text (should have been handled by extraction, but fallback)
try {
const fileContent = await fs.readFile(filePath, 'utf-8');
const previewContent = fileContent.length > 5000 ?
fileContent.substring(0, 5000) + '\n\n... (truncated)' :
fileContent;
return res.json({
success: true,
file: {
id: file.id,
originalName: file.originalName,
size: file.size,
uploadDate: file.uploadDate,
content: previewContent,
previewType: 'text'
}
});
} catch (readError) {
return res.json({
success: true,
file: {
id: file.id,
originalName: file.originalName,
size: file.size,
uploadDate: file.uploadDate,
content: 'File preview not available',
previewType: 'error',
message: `Error reading ${fileExtension.toUpperCase()} file: ${readError.message}`
}
});
}
} else {
// Binary format that couldn't be processed
return res.json({
success: true,
file: {
id: file.id,
originalName: file.originalName,
size: file.size,
uploadDate: file.uploadDate,
content: 'Preview not available for this file type. File content is available for AI processing.',
previewType: 'extraction-failed', previewType: 'extraction-failed',
message: `Failed to extract text from ${fileExtension.toUpperCase()} file: ${extractionResult.error}. The file has been uploaded and may still be usable for AI processing.` message: `Failed to extract text from ${fileExtension.toUpperCase()} file: ${extractionResult.error}. The file has been uploaded and may still be usable for AI processing.`
} }
@@ -2338,6 +2704,147 @@ app.listen(PORT, () => {
console.log(`EduCat server running on http://localhost:${PORT}`); console.log(`EduCat server running on http://localhost:${PORT}`);
}); });
// Helper function to convert markdown to safe HTML
function markdownToSafeHtml(markdownText) {
try {
// Configure marked options for better security and features
marked.setOptions({
gfm: true, // GitHub Flavored Markdown
breaks: true, // Convert line breaks to <br>
sanitize: false, // We'll use DOMPurify instead for better control
smartLists: true,
smartypants: true,
highlight: null // No code highlighting to avoid security issues
});
// Convert markdown to HTML
const rawHtml = marked.parse(markdownText);
// Sanitize the HTML with DOMPurify
const cleanHtml = DOMPurify.sanitize(rawHtml, {
ALLOWED_TAGS: [
'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
'p', 'br', 'hr',
'strong', 'b', 'em', 'i', 'u', 'mark',
'ul', 'ol', 'li',
'blockquote', 'pre', 'code',
'table', 'thead', 'tbody', 'tr', 'th', 'td',
'a', 'img',
'div', 'span'
],
ALLOWED_ATTR: [
'href', 'target', 'rel',
'src', 'alt', 'title',
'class', 'id',
'width', 'height'
],
ALLOW_DATA_ATTR: false,
FORBID_TAGS: ['script', 'object', 'embed', 'iframe', 'form', 'input'],
FORBID_ATTR: ['onclick', 'onload', 'onerror', 'style'],
ADD_ATTR: {
'a': { 'target': '_blank', 'rel': 'noopener noreferrer' }
}
});
return cleanHtml;
} catch (error) {
console.error('Error converting markdown to HTML:', error);
// Return escaped plain text as fallback
return escapeHtml(markdownText);
}
}
// Helper function to escape HTML
function escapeHtml(text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
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);

View File

@@ -101,12 +101,6 @@ document.addEventListener('DOMContentLoaded', function() {
• Explanations of complex concepts • Explanations of complex concepts
• Creating study plans and schedules • Creating study plans and schedules
I support **rich formatting** in my responses including:
- **Bold text** and *italic text*
- \`Code snippets\` and code blocks
- Numbered lists and bullet points
- Headers and structured content
How can I assist you today?`; How can I assist you today?`;
const formattedWelcome = formatMessage(welcomeText, true); const formattedWelcome = formatMessage(welcomeText, true);

View File

@@ -890,25 +890,100 @@ async function previewRevisedFile(fileId) {
const file = fileInfo.file; const file = fileInfo.file;
// Download the file content // Get file content using the new API endpoint
const contentResponse = await fetch(`/uploads/revised-notes/${file.filename}`); const contentResponse = await fetch(`/api/revised-files/${fileId}/content`);
if (!contentResponse.ok) { if (!contentResponse.ok) {
throw new Error('Failed to load file content'); throw new Error('Failed to load file content');
} }
const content = await contentResponse.text(); const contentResult = await contentResponse.json();
if (!contentResult.success) {
throw new Error('Failed to load file content');
}
const content = contentResult.content;
const modal = new bootstrap.Modal(document.getElementById('previewModal')); const modal = new bootstrap.Modal(document.getElementById('previewModal'));
// Create preview content with display mode toggle
document.getElementById('preview-content').innerHTML = ` document.getElementById('preview-content').innerHTML = `
<div class="alert alert-info"> <div class="d-flex justify-content-between align-items-center mb-3">
<i class="fas fa-brain me-2"></i> <div class="alert alert-info mb-0 flex-grow-1 me-3">
<strong>AI-Revised Content</strong> • ${file.revisionType} • From: ${file.originalFileName || 'Unknown'} <i class="fas fa-brain me-2"></i>
<strong>AI-Revised Content</strong> • ${file.revisionType} • From: ${file.originalFileName || 'Unknown'}
</div>
<div class="btn-group btn-group-sm" role="group" id="preview-display-mode">
<input type="radio" class="btn-check" name="previewDisplayMode" id="preview-mode-markdown" value="markdown" checked>
<label class="btn btn-outline-secondary" for="preview-mode-markdown" title="Show as Markdown">
<i class="fab fa-markdown"></i>
</label>
<input type="radio" class="btn-check" name="previewDisplayMode" id="preview-mode-html" value="html">
<label class="btn btn-outline-secondary" for="preview-mode-html" title="Render as HTML">
<i class="fas fa-code"></i>
</label>
</div>
</div> </div>
<div class="border p-3 bg-light rounded" style="max-height: 400px; overflow-y: auto;"> <div id="preview-content-container" class="border p-3 bg-light rounded" style="max-height: 400px; overflow-y: auto;">
<pre style="white-space: pre-wrap; word-wrap: break-word;">${escapeHtml(content)}</pre> <pre style="white-space: pre-wrap; word-wrap: break-word; margin: 0;">${escapeHtml(content)}</pre>
</div> </div>
`; `;
// Add event listeners for display mode toggle
const modeMarkdown = document.getElementById('preview-mode-markdown');
const modeHtml = document.getElementById('preview-mode-html');
const contentContainer = document.getElementById('preview-content-container');
async function updatePreviewMode() {
const selectedMode = document.querySelector('input[name="previewDisplayMode"]:checked').value;
try {
const renderResponse = await fetch('/api/render-revised-content', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
content: content,
displayMode: selectedMode,
autoDetect: true
})
});
const renderResult = await renderResponse.json();
if (renderResult.success) {
if (selectedMode === 'html') {
let htmlContent = `<div class="rendered-markdown">${renderResult.renderedContent}</div>`;
if (renderResult.isMarkdownContent) {
htmlContent = `
<div class="alert alert-info alert-sm mb-2">
<small><i class="fas fa-info-circle me-1"></i>Markdown formatting detected and rendered</small>
</div>
${htmlContent}
`;
}
contentContainer.innerHTML = htmlContent;
contentContainer.style.backgroundColor = '#ffffff';
} else {
contentContainer.innerHTML = `<pre style="white-space: pre-wrap; word-wrap: break-word; margin: 0;">${escapeHtml(renderResult.renderedContent)}</pre>`;
contentContainer.style.backgroundColor = '#f8f9fa';
}
} else {
throw new Error(renderResult.error || 'Failed to render content');
}
} catch (error) {
console.error('Error rendering preview:', error);
// Fallback to escaped text
contentContainer.innerHTML = `<pre style="white-space: pre-wrap; word-wrap: break-word; margin: 0;">${escapeHtml(content)}</pre>`;
contentContainer.style.backgroundColor = '#f8f9fa';
}
}
modeMarkdown.addEventListener('change', updatePreviewMode);
modeHtml.addEventListener('change', updatePreviewMode);
modal.show(); modal.show();
} catch (error) { } catch (error) {
console.error('Error previewing revised file:', error); console.error('Error previewing revised file:', error);

View File

@@ -35,7 +35,20 @@
</div> </div>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<h5>AI-Revised Notes</h5> <div class="d-flex justify-content-between align-items-center mb-2">
<h5>AI-Revised Notes</h5>
<div class="btn-group btn-group-sm" role="group" id="display-mode-toggle" style="display: none;">
<input type="radio" class="btn-check" name="displayMode" id="mode-markdown" value="markdown" checked>
<label class="btn btn-outline-secondary" for="mode-markdown" title="Show as Markdown">
<i class="fab fa-markdown"></i>
</label>
<input type="radio" class="btn-check" name="displayMode" id="mode-html" value="html">
<label class="btn btn-outline-secondary" for="mode-html" title="Render as HTML">
<i class="fas fa-code"></i>
</label>
</div>
</div>
<div id="revised-content" class="border p-3 bg-white rounded" style="height: 400px; overflow-y: auto;"> <div id="revised-content" class="border p-3 bg-white rounded" style="height: 400px; overflow-y: auto;">
<p class="text-muted text-center mt-5">Select a revision type and click "Revise" to see AI-enhanced notes here.</p> <p class="text-muted text-center mt-5">Select a revision type and click "Revise" to see AI-enhanced notes here.</p>
</div> </div>
@@ -139,11 +152,72 @@ document.addEventListener('DOMContentLoaded', function() {
const revisionType = document.getElementById('revision-type'); const revisionType = document.getElementById('revision-type');
const saveBtn = document.getElementById('save-btn'); const saveBtn = document.getElementById('save-btn');
const downloadBtn = document.getElementById('download-btn'); const downloadBtn = document.getElementById('download-btn');
const displayModeToggle = document.getElementById('display-mode-toggle');
const modeMarkdown = document.getElementById('mode-markdown');
const modeHtml = document.getElementById('mode-html');
const fileId = '<%= file.id %>'; const fileId = '<%= file.id %>';
const content = <%- JSON.stringify(content) %>; const content = <%- JSON.stringify(content) %>;
let currentRevisedContent = ''; let currentRevisedContent = '';
let currentRevisionType = ''; let currentRevisionType = '';
let currentDisplayMode = 'markdown';
// Handle display mode changes
function updateDisplayMode() {
if (!currentRevisedContent) return;
const selectedMode = document.querySelector('input[name="displayMode"]:checked').value;
currentDisplayMode = selectedMode;
renderRevisedContent(currentRevisedContent, selectedMode);
}
// Render revised content based on display mode
async function renderRevisedContent(content, displayMode = 'markdown') {
try {
const response = await fetch('/api/render-revised-content', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
content: content,
displayMode: displayMode,
autoDetect: true
})
});
const result = await response.json();
if (result.success) {
if (displayMode === 'html') {
// Render as HTML
revisedContent.innerHTML = `<div class="rendered-markdown">${result.renderedContent}</div>`;
// Show format detection info if available
if (result.isMarkdownContent) {
const formatInfo = document.createElement('div');
formatInfo.className = 'alert alert-info alert-sm mb-2';
formatInfo.innerHTML = '<small><i class="fas fa-info-circle me-1"></i>Markdown formatting detected and rendered</small>';
revisedContent.insertBefore(formatInfo, revisedContent.firstChild);
}
} else {
// Show as raw markdown/text
revisedContent.innerHTML = `<pre class="mb-0" style="white-space: pre-wrap; word-wrap: break-word;">${escapeHtml(result.renderedContent)}</pre>`;
}
} else {
throw new Error(result.error || 'Failed to render content');
}
} catch (error) {
console.error('Error rendering content:', error);
// Fallback to escaped text
revisedContent.innerHTML = `<pre class="mb-0" style="white-space: pre-wrap; word-wrap: break-word;">${escapeHtml(content)}</pre>`;
}
}
// Add event listeners for display mode toggle
modeMarkdown.addEventListener('change', updateDisplayMode);
modeHtml.addEventListener('change', updateDisplayMode);
reviseBtn.addEventListener('click', async function() { reviseBtn.addEventListener('click', async function() {
const type = revisionType.value; const type = revisionType.value;
@@ -153,6 +227,7 @@ document.addEventListener('DOMContentLoaded', function() {
reviseBtn.disabled = true; reviseBtn.disabled = true;
revisionProgress.classList.remove('d-none'); revisionProgress.classList.remove('d-none');
displayModeToggle.style.display = 'none'; // Hide toggle during processing
revisedContent.innerHTML = '<div class="text-center mt-5"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Processing...</span></div><p class="mt-2">AI is processing your notes...</p></div>'; revisedContent.innerHTML = '<div class="text-center mt-5"><div class="spinner-border text-primary" role="status"><span class="visually-hidden">Processing...</span></div><p class="mt-2">AI is processing your notes...</p></div>';
try { try {
@@ -175,9 +250,15 @@ document.addEventListener('DOMContentLoaded', function() {
console.log('Revision result:', result); console.log('Revision result:', result);
if (result.success) { if (result.success) {
revisedContent.innerHTML = '<pre class="mb-0" style="white-space: pre-wrap; word-wrap: break-word;">' + escapeHtml(result.revisedContent) + '</pre>';
currentRevisedContent = result.revisedContent; currentRevisedContent = result.revisedContent;
currentRevisionType = type; currentRevisionType = type;
// Show display mode toggle
displayModeToggle.style.display = 'block';
// Render content based on current display mode
await renderRevisedContent(currentRevisedContent, currentDisplayMode);
saveBtn.disabled = false; saveBtn.disabled = false;
downloadBtn.disabled = false; downloadBtn.disabled = false;
} else { } else {