Buggy but working
This commit is contained in:
parent
1ce63a8fba
commit
109d43f585
8 changed files with 753 additions and 38 deletions
7
.env.template
Normal file
7
.env.template
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
# OpenAI API Key for Text-to-Speech functionality
|
||||||
|
NEXT_PUBLIC_OPENAI_API_KEY=your_openai_api_key_here
|
||||||
|
|
||||||
|
# OpenAI API Base URL (default)
|
||||||
|
NEXT_PUBLIC_OPENAI_API_BASE=https://api.openai.com/v1
|
||||||
|
|
||||||
|
# Add other environment variables below as needed
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -31,7 +31,7 @@ yarn-error.log*
|
||||||
.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
|
|
||||||
# env files (can opt-in for committing if needed)
|
# env files (can opt-in for committing if needed)
|
||||||
.env*
|
.env
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
|
|
||||||
289
package-lock.json
generated
289
package-lock.json
generated
|
|
@ -9,13 +9,17 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^2.2.0",
|
"@headlessui/react": "^2.2.0",
|
||||||
|
"@types/string-similarity": "^4.0.2",
|
||||||
"compromise": "^14.14.4",
|
"compromise": "^14.14.4",
|
||||||
|
"lru-cache": "^11.0.2",
|
||||||
"next": "15.1.5",
|
"next": "15.1.5",
|
||||||
|
"openai": "^4.79.1",
|
||||||
"pdfjs-dist": "4.8.69",
|
"pdfjs-dist": "4.8.69",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-dropzone": "^14.3.5",
|
"react-dropzone": "^14.3.5",
|
||||||
"react-pdf": "^9.2.1"
|
"react-pdf": "^9.2.1",
|
||||||
|
"string-similarity": "^4.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
|
|
@ -1120,12 +1124,21 @@
|
||||||
"version": "20.17.14",
|
"version": "20.17.14",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.14.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.14.tgz",
|
||||||
"integrity": "sha512-w6qdYetNL5KRBiSClK/KWai+2IMEJuAj+EujKCumalFOwXtvOXaEan9AuwcRID2IcOIAWSIfR495hBtgKlx2zg==",
|
"integrity": "sha512-w6qdYetNL5KRBiSClK/KWai+2IMEJuAj+EujKCumalFOwXtvOXaEan9AuwcRID2IcOIAWSIfR495hBtgKlx2zg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.19.2"
|
"undici-types": "~6.19.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/node-fetch": {
|
||||||
|
"version": "2.6.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.12.tgz",
|
||||||
|
"integrity": "sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*",
|
||||||
|
"form-data": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "19.0.7",
|
"version": "19.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.7.tgz",
|
||||||
|
|
@ -1146,6 +1159,12 @@
|
||||||
"@types/react": "^19.0.0"
|
"@types/react": "^19.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/string-similarity": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/string-similarity/-/string-similarity-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-LkJQ/jsXtCVMK+sKYAmX/8zEq+/46f1PTQw7YtmQwb74jemS1SlNLmARM2Zml9DgdDTWKAtc5L13WorpHPDjDA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.20.0",
|
"version": "8.20.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.20.0.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.20.0.tgz",
|
||||||
|
|
@ -1382,6 +1401,18 @@
|
||||||
"url": "https://opencollective.com/typescript-eslint"
|
"url": "https://opencollective.com/typescript-eslint"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/abort-controller": {
|
||||||
|
"version": "3.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
|
||||||
|
"integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"event-target-shim": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/acorn": {
|
"node_modules/acorn": {
|
||||||
"version": "8.14.0",
|
"version": "8.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz",
|
||||||
|
|
@ -1405,6 +1436,18 @@
|
||||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/agentkeepalive": {
|
||||||
|
"version": "4.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
|
||||||
|
"integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"humanize-ms": "^1.2.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 8.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "6.12.6",
|
"version": "6.12.6",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
|
|
@ -1660,6 +1703,12 @@
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/asynckit": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/attr-accept": {
|
"node_modules/attr-accept": {
|
||||||
"version": "2.2.5",
|
"version": "2.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz",
|
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz",
|
||||||
|
|
@ -2046,6 +2095,18 @@
|
||||||
"simple-swizzle": "^0.2.2"
|
"simple-swizzle": "^0.2.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/combined-stream": {
|
||||||
|
"version": "1.0.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||||
|
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"delayed-stream": "~1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.8"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/commander": {
|
"node_modules/commander": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||||
|
|
@ -2257,6 +2318,15 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/delayed-stream": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dequal": {
|
"node_modules/dequal": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
|
||||||
|
|
@ -3005,6 +3075,15 @@
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/event-target-shim": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/expand-template": {
|
"node_modules/expand-template": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
||||||
|
|
@ -3179,6 +3258,39 @@
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/form-data": {
|
||||||
|
"version": "4.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
|
||||||
|
"integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"asynckit": "^0.4.0",
|
||||||
|
"combined-stream": "^1.0.8",
|
||||||
|
"mime-types": "^2.1.12"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/form-data-encoder": {
|
||||||
|
"version": "1.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
|
||||||
|
"integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/formdata-node": {
|
||||||
|
"version": "4.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
|
||||||
|
"integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"node-domexception": "1.0.0",
|
||||||
|
"web-streams-polyfill": "4.0.0-beta.3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12.20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fs-constants": {
|
"node_modules/fs-constants": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||||
|
|
@ -3539,6 +3651,15 @@
|
||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/humanize-ms": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"ms": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ieee754": {
|
"node_modules/ieee754": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
|
||||||
|
|
@ -4270,11 +4391,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "10.4.3",
|
"version": "11.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz",
|
||||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
"integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==",
|
||||||
"dev": true,
|
"license": "ISC",
|
||||||
"license": "ISC"
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/make-cancellable-promise": {
|
"node_modules/make-cancellable-promise": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.2",
|
||||||
|
|
@ -4345,6 +4468,27 @@
|
||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/mime-db": {
|
||||||
|
"version": "1.52.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||||
|
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/mime-types": {
|
||||||
|
"version": "2.1.35",
|
||||||
|
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||||
|
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"mime-db": "1.52.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/mimic-response": {
|
"node_modules/mimic-response": {
|
||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz",
|
||||||
|
|
@ -4402,7 +4546,6 @@
|
||||||
"version": "2.1.3",
|
"version": "2.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/mz": {
|
"node_modules/mz": {
|
||||||
|
|
@ -4551,6 +4694,45 @@
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/node-domexception": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/jimmywarting"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://paypal.me/jimmywarting"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10.5.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/node-fetch": {
|
||||||
|
"version": "2.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
|
||||||
|
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"whatwg-url": "^5.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "4.x || >=6.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"encoding": "^0.1.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"encoding": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/normalize-path": {
|
"node_modules/normalize-path": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||||
|
|
@ -4702,6 +4884,51 @@
|
||||||
"wrappy": "1"
|
"wrappy": "1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/openai": {
|
||||||
|
"version": "4.79.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/openai/-/openai-4.79.1.tgz",
|
||||||
|
"integrity": "sha512-M7P5/PKnT/S/B5v0D64giC9mjyxFYkqlCuQFzR5hkdzMdqUuHf8T1gHhPGPF5oAvu4+PO3TvJv/qhZoS2bqAkw==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "^18.11.18",
|
||||||
|
"@types/node-fetch": "^2.6.4",
|
||||||
|
"abort-controller": "^3.0.0",
|
||||||
|
"agentkeepalive": "^4.2.1",
|
||||||
|
"form-data-encoder": "1.7.2",
|
||||||
|
"formdata-node": "^4.3.2",
|
||||||
|
"node-fetch": "^2.6.7"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"openai": "bin/cli"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"ws": "^8.18.0",
|
||||||
|
"zod": "^3.23.8"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"ws": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"zod": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/openai/node_modules/@types/node": {
|
||||||
|
"version": "18.19.71",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.71.tgz",
|
||||||
|
"integrity": "sha512-evXpcgtZm8FY4jqBSN8+DmOTcVkkvTmAayeo4Wf3m1xAruyVGzGuDh/Fb/WWX2yLItUiho42ozyJjB0dw//Tkw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"undici-types": "~5.26.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/openai/node_modules/undici-types": {
|
||||||
|
"version": "5.26.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||||
|
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
|
|
@ -4834,6 +5061,13 @@
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/path-scurry/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==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/path2d": {
|
"node_modules/path2d": {
|
||||||
"version": "0.2.2",
|
"version": "0.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/path2d/-/path2d-0.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/path2d/-/path2d-0.2.2.tgz",
|
||||||
|
|
@ -5831,6 +6065,13 @@
|
||||||
"safe-buffer": "~5.2.0"
|
"safe-buffer": "~5.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/string-similarity": {
|
||||||
|
"version": "4.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/string-similarity/-/string-similarity-4.0.4.tgz",
|
||||||
|
"integrity": "sha512-/q/8Q4Bl4ZKAPjj8WerIBJWALKkaPRfrvhfF8k/B23i4nzrlRj2/go1m90In7nG/3XDSbOo0+pu6RvCTM9RGMQ==",
|
||||||
|
"deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/string-width": {
|
"node_modules/string-width": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||||
|
|
@ -6305,6 +6546,12 @@
|
||||||
"node": ">=8.0"
|
"node": ">=8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tr46": {
|
||||||
|
"version": "0.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
|
||||||
|
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/ts-api-utils": {
|
"node_modules/ts-api-utils": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.0.tgz",
|
||||||
|
|
@ -6485,7 +6732,6 @@
|
||||||
"version": "6.19.8",
|
"version": "6.19.8",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
||||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/uri-js": {
|
"node_modules/uri-js": {
|
||||||
|
|
@ -6514,6 +6760,31 @@
|
||||||
"loose-envify": "^1.0.0"
|
"loose-envify": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/web-streams-polyfill": {
|
||||||
|
"version": "4.0.0-beta.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
|
||||||
|
"integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 14"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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-url": {
|
||||||
|
"version": "5.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
|
||||||
|
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tr46": "~0.0.3",
|
||||||
|
"webidl-conversions": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,17 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^2.2.0",
|
"@headlessui/react": "^2.2.0",
|
||||||
|
"@types/string-similarity": "^4.0.2",
|
||||||
"compromise": "^14.14.4",
|
"compromise": "^14.14.4",
|
||||||
|
"lru-cache": "^11.0.2",
|
||||||
"next": "15.1.5",
|
"next": "15.1.5",
|
||||||
|
"openai": "^4.79.1",
|
||||||
"pdfjs-dist": "4.8.69",
|
"pdfjs-dist": "4.8.69",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-dropzone": "^14.3.5",
|
"react-dropzone": "^14.3.5",
|
||||||
"react-pdf": "^9.2.1"
|
"react-pdf": "^9.2.1",
|
||||||
|
"string-similarity": "^4.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import type { Metadata } from "next";
|
||||||
import { Geist, Geist_Mono } from "next/font/google";
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { Providers } from "./providers";
|
import { Providers } from "./providers";
|
||||||
import { TTSProvider } from "@/context/TTSContext";
|
|
||||||
import TTSPlayer from "@/components/TTSPlayer";
|
import TTSPlayer from "@/components/TTSPlayer";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const geistSans = Geist({
|
||||||
|
|
@ -31,16 +30,14 @@ export default function RootLayout({
|
||||||
className={`${geistSans.variable} ${geistMono.variable} font-sans antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} font-sans antialiased`}
|
||||||
>
|
>
|
||||||
<Providers>
|
<Providers>
|
||||||
<TTSProvider>
|
<div className="min-h-screen bg-background p-4">
|
||||||
<div className="min-h-screen bg-background p-4">
|
<div className="max-w-6xl mx-auto align-center">
|
||||||
<div className="max-w-6xl mx-auto align-center">
|
<div className="bg-base rounded-lg shadow-lg">
|
||||||
<div className="bg-base rounded-lg shadow-lg">
|
{children}
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<TTSPlayer />
|
</div>
|
||||||
</TTSProvider>
|
<TTSPlayer />
|
||||||
</Providers>
|
</Providers>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,15 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { PDFProvider } from '@/context/PDFContext';
|
import { PDFProvider } from '@/context/PDFContext';
|
||||||
|
import { TTSProvider } from '@/context/TTSContext';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
|
||||||
export function Providers({ children }: { children: ReactNode }) {
|
export function Providers({ children }: { children: ReactNode }) {
|
||||||
return <PDFProvider>{children}</PDFProvider>;
|
return (
|
||||||
|
<TTSProvider>
|
||||||
|
<PDFProvider>
|
||||||
|
{children}
|
||||||
|
</PDFProvider>
|
||||||
|
</TTSProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,10 @@
|
||||||
import { Document, Page, pdfjs } from 'react-pdf';
|
import { Document, Page, pdfjs } from 'react-pdf';
|
||||||
import 'react-pdf/dist/Page/AnnotationLayer.css';
|
import 'react-pdf/dist/Page/AnnotationLayer.css';
|
||||||
import 'react-pdf/dist/Page/TextLayer.css';
|
import 'react-pdf/dist/Page/TextLayer.css';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { PDFSkeleton } from './PDFSkeleton';
|
import { PDFSkeleton } from './PDFSkeleton';
|
||||||
import { useTTS } from '@/context/TTSContext';
|
import { useTTS } from '@/context/TTSContext';
|
||||||
|
import stringSimilarity from 'string-similarity';
|
||||||
|
|
||||||
// Set worker from public directory
|
// Set worker from public directory
|
||||||
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.mjs';
|
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.mjs';
|
||||||
|
|
@ -14,24 +15,179 @@ interface PDFViewerProps {
|
||||||
pdfFile: string | undefined;
|
pdfFile: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TextHighlight {
|
||||||
|
pageIndex: number;
|
||||||
|
content: string;
|
||||||
|
position: {
|
||||||
|
boundingRect: DOMRect;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function PDFViewer({ pdfFile }: PDFViewerProps) {
|
export function PDFViewer({ pdfFile }: PDFViewerProps) {
|
||||||
const [numPages, setNumPages] = useState<number>();
|
const [numPages, setNumPages] = useState<number>();
|
||||||
const { setText } = useTTS();
|
const { setText, currentSentence } = useTTS();
|
||||||
const [pdfText, setPdfText] = useState('');
|
const [pdfText, setPdfText] = useState('');
|
||||||
|
const [highlights, setHighlights] = useState<TextHighlight[]>([]);
|
||||||
|
|
||||||
|
// Function to clear all highlights
|
||||||
|
const clearHighlights = useCallback(() => {
|
||||||
|
const textNodes = document.querySelectorAll('.react-pdf__Page__textContent span');
|
||||||
|
textNodes.forEach((node) => {
|
||||||
|
const element = node as HTMLElement;
|
||||||
|
element.style.backgroundColor = '';
|
||||||
|
element.style.opacity = '1';
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
function onDocumentLoadSuccess({ numPages }: { numPages: number }): void {
|
function onDocumentLoadSuccess({ numPages }: { numPages: number }): void {
|
||||||
setNumPages(numPages);
|
setNumPages(numPages);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const findBestMatch = useCallback((searchText: string, content: string) => {
|
||||||
|
if (!searchText?.trim() || !content?.trim()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Split content into chunks roughly the size of the search text
|
||||||
|
const chunkSize = searchText.length + 20; // Add some padding
|
||||||
|
const chunks: string[] = [];
|
||||||
|
|
||||||
|
// Ensure we have valid content to split
|
||||||
|
const cleanContent = content.trim();
|
||||||
|
if (cleanContent.length < chunkSize) {
|
||||||
|
chunks.push(cleanContent);
|
||||||
|
} else {
|
||||||
|
for (let i = 0; i < cleanContent.length - chunkSize; i += chunkSize / 2) {
|
||||||
|
const chunk = cleanContent.slice(i, i + chunkSize).trim();
|
||||||
|
if (chunk) {
|
||||||
|
chunks.push(chunk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure we have valid chunks before matching
|
||||||
|
if (chunks.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the best matching chunk
|
||||||
|
const matches = stringSimilarity.findBestMatch(searchText.trim(), chunks);
|
||||||
|
return matches.bestMatch.rating >= 0.5 ? matches.bestMatch : null;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error in findBestMatch:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const highlightPattern = useCallback((text: string, pattern: string) => {
|
||||||
|
console.log('Highlighting pattern:', pattern);
|
||||||
|
|
||||||
|
// Always clear existing highlights first
|
||||||
|
clearHighlights();
|
||||||
|
|
||||||
|
if (!pattern?.trim()) {
|
||||||
|
console.log('No pattern to highlight');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up the pattern
|
||||||
|
const cleanPattern = pattern.trim().replace(/\s+/g, ' ');
|
||||||
|
const patternLength = cleanPattern.length;
|
||||||
|
console.log('Clean pattern:', cleanPattern, 'Length:', patternLength);
|
||||||
|
|
||||||
|
// Get all text nodes
|
||||||
|
const textNodes = document.querySelectorAll('.react-pdf__Page__textContent span');
|
||||||
|
const allText = Array.from(textNodes).map(node => ({
|
||||||
|
element: node as HTMLElement,
|
||||||
|
text: (node.textContent || '').trim()
|
||||||
|
})).filter(node => node.text.length > 0); // Remove empty nodes
|
||||||
|
|
||||||
|
// Find the best matching position
|
||||||
|
let bestMatch = {
|
||||||
|
elements: [] as HTMLElement[],
|
||||||
|
rating: 0,
|
||||||
|
text: '',
|
||||||
|
lengthDiff: Infinity
|
||||||
|
};
|
||||||
|
|
||||||
|
// Try different combinations of consecutive spans
|
||||||
|
for (let i = 0; i < allText.length; i++) {
|
||||||
|
let combinedText = '';
|
||||||
|
let currentElements = [];
|
||||||
|
|
||||||
|
// Look ahead up to 10 spans, but stop if we exceed 2x pattern length
|
||||||
|
for (let j = i; j < Math.min(i + 10, allText.length); j++) {
|
||||||
|
const node = allText[j];
|
||||||
|
const newText = combinedText + (combinedText ? ' ' : '') + node.text;
|
||||||
|
|
||||||
|
// Stop if we're getting too far from target length
|
||||||
|
if (newText.length > patternLength * 2) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
combinedText = newText;
|
||||||
|
currentElements.push(node.element);
|
||||||
|
|
||||||
|
// Calculate similarity and length difference
|
||||||
|
const similarity = stringSimilarity.compareTwoStrings(cleanPattern, combinedText);
|
||||||
|
const lengthDiff = Math.abs(combinedText.length - patternLength);
|
||||||
|
|
||||||
|
// Score based on both similarity and length difference
|
||||||
|
const lengthPenalty = lengthDiff / patternLength; // Normalized length difference
|
||||||
|
const adjustedRating = similarity * (1 - lengthPenalty * 0.5); // Reduce score based on length difference
|
||||||
|
|
||||||
|
console.log('Comparing:', {
|
||||||
|
text: combinedText,
|
||||||
|
similarity,
|
||||||
|
lengthDiff,
|
||||||
|
adjustedRating
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update best match if we have better adjusted rating
|
||||||
|
if (adjustedRating > bestMatch.rating) {
|
||||||
|
bestMatch = {
|
||||||
|
elements: [...currentElements],
|
||||||
|
rating: adjustedRating,
|
||||||
|
text: combinedText,
|
||||||
|
lengthDiff
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only highlight if we found a good match
|
||||||
|
// Adjust threshold based on match quality
|
||||||
|
const similarityThreshold = bestMatch.lengthDiff < patternLength * 0.3 ? 0.4 : 0.6;
|
||||||
|
|
||||||
|
if (bestMatch.rating >= similarityThreshold) {
|
||||||
|
console.log('Found match:', {
|
||||||
|
rating: bestMatch.rating,
|
||||||
|
text: bestMatch.text,
|
||||||
|
lengthDiff: bestMatch.lengthDiff,
|
||||||
|
threshold: similarityThreshold
|
||||||
|
});
|
||||||
|
|
||||||
|
bestMatch.elements.forEach(element => {
|
||||||
|
element.style.backgroundColor = 'yellow';
|
||||||
|
element.style.opacity = '0.4';
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.log('No good match found:', {
|
||||||
|
bestRating: bestMatch.rating,
|
||||||
|
bestText: bestMatch.text,
|
||||||
|
threshold: similarityThreshold
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [clearHighlights]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (pdfFile) {
|
if (pdfFile) {
|
||||||
// Load PDF text when file changes
|
|
||||||
const loadPdfText = async () => {
|
const loadPdfText = async () => {
|
||||||
try {
|
try {
|
||||||
const pdf = await pdfjs.getDocument(pdfFile).promise;
|
const pdf = await pdfjs.getDocument(pdfFile).promise;
|
||||||
let fullText = '';
|
let fullText = '';
|
||||||
|
|
||||||
// Get text from all pages
|
|
||||||
for (let i = 1; i <= pdf.numPages; i++) {
|
for (let i = 1; i <= pdf.numPages; i++) {
|
||||||
const page = await pdf.getPage(i);
|
const page = await pdf.getPage(i);
|
||||||
const textContent = await page.getTextContent();
|
const textContent = await page.getTextContent();
|
||||||
|
|
@ -41,6 +197,7 @@ export function PDFViewer({ pdfFile }: PDFViewerProps) {
|
||||||
fullText += pageText + ' ';
|
fullText += pageText + ' ';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('Loaded PDF text sample:', fullText.substring(0, 100));
|
||||||
setPdfText(fullText);
|
setPdfText(fullText);
|
||||||
setText(fullText);
|
setText(fullText);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -50,7 +207,25 @@ export function PDFViewer({ pdfFile }: PDFViewerProps) {
|
||||||
|
|
||||||
loadPdfText();
|
loadPdfText();
|
||||||
}
|
}
|
||||||
}, [pdfFile, setText]);
|
|
||||||
|
// Clear highlights when PDF changes
|
||||||
|
return () => clearHighlights();
|
||||||
|
}, [pdfFile, setText, clearHighlights]);
|
||||||
|
|
||||||
|
// Update highlights when current sentence changes
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('Current sentence changed:', currentSentence);
|
||||||
|
|
||||||
|
const highlightTimeout = setTimeout(() => {
|
||||||
|
highlightPattern(pdfText, currentSentence || '');
|
||||||
|
}, 100); // Quick response for TTS
|
||||||
|
|
||||||
|
// Clear highlights when effect is cleaned up
|
||||||
|
return () => {
|
||||||
|
clearTimeout(highlightTimeout);
|
||||||
|
clearHighlights();
|
||||||
|
};
|
||||||
|
}, [pdfText, currentSentence, highlightPattern, clearHighlights]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,27 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { createContext, useContext, useState, useCallback } from 'react';
|
import React, {
|
||||||
|
createContext,
|
||||||
|
useContext,
|
||||||
|
useState,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
} from 'react';
|
||||||
import nlp from 'compromise';
|
import nlp from 'compromise';
|
||||||
|
import OpenAI from 'openai';
|
||||||
|
import { LRUCache } from 'lru-cache'; // Import LRUCache directly
|
||||||
|
|
||||||
|
// Add type declarations
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
webkitAudioContext: typeof AudioContext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type AudioContextType = typeof window extends undefined
|
||||||
|
? never
|
||||||
|
: AudioContext | null;
|
||||||
|
|
||||||
interface TTSContextType {
|
interface TTSContextType {
|
||||||
isPlaying: boolean;
|
isPlaying: boolean;
|
||||||
|
|
@ -11,6 +31,8 @@ interface TTSContextType {
|
||||||
skipBackward: () => void;
|
skipBackward: () => void;
|
||||||
setText: (text: string) => void;
|
setText: (text: string) => void;
|
||||||
currentSentence: string;
|
currentSentence: string;
|
||||||
|
audioQueue: AudioBuffer[];
|
||||||
|
currentAudioIndex: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TTSContext = createContext<TTSContextType | undefined>(undefined);
|
const TTSContext = createContext<TTSContextType | undefined>(undefined);
|
||||||
|
|
@ -20,6 +42,40 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [currentText, setCurrentText] = useState('');
|
const [currentText, setCurrentText] = useState('');
|
||||||
const [sentences, setSentences] = useState<string[]>([]);
|
const [sentences, setSentences] = useState<string[]>([]);
|
||||||
const [currentIndex, setCurrentIndex] = useState(0);
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
|
const [audioContext, setAudioContext] = useState<AudioContextType>(null);
|
||||||
|
const currentRequestRef = useRef<AbortController | null>(null);
|
||||||
|
const [activeSource, setActiveSource] = useState<AudioBufferSourceNode | null>(null);
|
||||||
|
const [audioQueue, setAudioQueue] = useState<AudioBuffer[]>([]);
|
||||||
|
const [currentAudioIndex, setCurrentAudioIndex] = useState(0);
|
||||||
|
const [isProcessing, setIsProcessing] = useState(false);
|
||||||
|
const skipTriggeredRef = useRef(false);
|
||||||
|
|
||||||
|
// Create OpenAI instance
|
||||||
|
const [openai] = useState(
|
||||||
|
() =>
|
||||||
|
new OpenAI({
|
||||||
|
apiKey: process.env.NEXT_PUBLIC_OPENAI_API_KEY,
|
||||||
|
baseURL: process.env.NEXT_PUBLIC_OPENAI_API_BASE,
|
||||||
|
dangerouslyAllowBrowser: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Audio cache using LRUCache with a maximum size of 50 entries
|
||||||
|
const audioCacheRef = useRef(new LRUCache<string, AudioBuffer>({ max: 50 }));
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Initialize AudioContext on client side only
|
||||||
|
if (typeof window !== 'undefined' && !audioContext) {
|
||||||
|
const AudioContextClass = window.AudioContext || window.webkitAudioContext;
|
||||||
|
if (AudioContextClass) {
|
||||||
|
try {
|
||||||
|
setAudioContext(new AudioContextClass());
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize AudioContext:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [audioContext]);
|
||||||
|
|
||||||
const splitIntoSentences = (text: string): string[] => {
|
const splitIntoSentences = (text: string): string[] => {
|
||||||
const doc = nlp(text);
|
const doc = nlp(text);
|
||||||
|
|
@ -27,37 +83,233 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
||||||
return doc.sentences().out('array') as string[];
|
return doc.sentences().out('array') as string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const processNextSentence = useCallback(async () => {
|
||||||
|
if (!isPlaying || currentIndex >= sentences.length - 1 || skipTriggeredRef.current) {
|
||||||
|
setIsPlaying(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentIndex((prev) => {
|
||||||
|
const nextIndex = prev + 1;
|
||||||
|
if (nextIndex < sentences.length) {
|
||||||
|
console.log('Auto-advancing to next sentence:', sentences[nextIndex]);
|
||||||
|
return nextIndex;
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
});
|
||||||
|
}, [isPlaying, currentIndex, sentences]);
|
||||||
|
|
||||||
|
const processAndPlaySentence = async (sentence: string) => {
|
||||||
|
if (!audioContext || isProcessing) return;
|
||||||
|
setIsProcessing(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Cancel any existing request
|
||||||
|
if (currentRequestRef.current) {
|
||||||
|
currentRequestRef.current.abort();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new abort controller for this request
|
||||||
|
currentRequestRef.current = new AbortController();
|
||||||
|
|
||||||
|
// Stop any currently playing audio
|
||||||
|
if (activeSource) {
|
||||||
|
activeSource.stop();
|
||||||
|
setActiveSource(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
let audioBuffer = audioCacheRef.current.get(sentence);
|
||||||
|
|
||||||
|
if (!audioBuffer) {
|
||||||
|
console.log(' Processing TTS for sentence:', sentence.substring(0, 50) + '...');
|
||||||
|
const startTime = Date.now();
|
||||||
|
const response = await openai.audio.speech.create({
|
||||||
|
model: 'tts-1',
|
||||||
|
voice: 'alloy',
|
||||||
|
input: sentence,
|
||||||
|
});
|
||||||
|
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
console.log(` TTS processing completed in ${duration}ms`);
|
||||||
|
|
||||||
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
||||||
|
|
||||||
|
// Store in cache
|
||||||
|
audioCacheRef.current.set(sentence, audioBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the request was aborted, do not proceed
|
||||||
|
if (!currentRequestRef.current) return;
|
||||||
|
|
||||||
|
const source = audioContext.createBufferSource();
|
||||||
|
source.buffer = audioBuffer;
|
||||||
|
source.connect(audioContext.destination);
|
||||||
|
setActiveSource(source);
|
||||||
|
|
||||||
|
// Set up onended handler before starting
|
||||||
|
source.onended = () => {
|
||||||
|
setActiveSource(null);
|
||||||
|
setIsProcessing(false);
|
||||||
|
if (isPlaying && !skipTriggeredRef.current) {
|
||||||
|
processNextSentence();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
source.start(0);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error && error.name === 'AbortError') {
|
||||||
|
console.log('Request was cancelled');
|
||||||
|
} else {
|
||||||
|
console.error('Error processing TTS:', error);
|
||||||
|
}
|
||||||
|
setActiveSource(null);
|
||||||
|
setIsProcessing(false);
|
||||||
|
} finally {
|
||||||
|
currentRequestRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const togglePlay = useCallback(() => {
|
const togglePlay = useCallback(() => {
|
||||||
setIsPlaying(prev => !prev);
|
setIsPlaying((prev) => {
|
||||||
// TODO: Implement actual TTS play/pause logic
|
if (!prev) {
|
||||||
}, []);
|
return true; // Start playing
|
||||||
|
} else {
|
||||||
|
// Pause playback
|
||||||
|
if (activeSource) {
|
||||||
|
activeSource.stop(); // Stop the audio source
|
||||||
|
setActiveSource(null); // Clear the active source
|
||||||
|
}
|
||||||
|
return false; // Set isPlaying to false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, [activeSource]);
|
||||||
|
|
||||||
const skipForward = useCallback(() => {
|
const skipForward = useCallback(() => {
|
||||||
setCurrentIndex(prev => {
|
skipTriggeredRef.current = true;
|
||||||
|
// Cancel any ongoing request
|
||||||
|
if (currentRequestRef.current) {
|
||||||
|
currentRequestRef.current.abort();
|
||||||
|
currentRequestRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop current audio
|
||||||
|
if (activeSource) {
|
||||||
|
activeSource.stop();
|
||||||
|
setActiveSource(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentIndex((prev) => {
|
||||||
const nextIndex = Math.min(prev + 1, sentences.length - 1);
|
const nextIndex = Math.min(prev + 1, sentences.length - 1);
|
||||||
console.log('Current sentence:', sentences[nextIndex]);
|
console.log('Skipping forward to:', sentences[nextIndex]);
|
||||||
return nextIndex;
|
return nextIndex;
|
||||||
});
|
});
|
||||||
}, [sentences]);
|
|
||||||
|
// Reset skip flag after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
skipTriggeredRef.current = false;
|
||||||
|
}, 100);
|
||||||
|
}, [sentences, activeSource]);
|
||||||
|
|
||||||
const skipBackward = useCallback(() => {
|
const skipBackward = useCallback(() => {
|
||||||
setCurrentIndex(prev => {
|
skipTriggeredRef.current = true;
|
||||||
|
// Cancel any ongoing request
|
||||||
|
if (currentRequestRef.current) {
|
||||||
|
currentRequestRef.current.abort();
|
||||||
|
currentRequestRef.current = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop current audio
|
||||||
|
if (activeSource) {
|
||||||
|
activeSource.stop();
|
||||||
|
setActiveSource(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentIndex((prev) => {
|
||||||
const nextIndex = Math.max(prev - 1, 0);
|
const nextIndex = Math.max(prev - 1, 0);
|
||||||
console.log('Current sentence:', sentences[nextIndex]);
|
console.log('Skipping backward to:', sentences[nextIndex]);
|
||||||
return nextIndex;
|
return nextIndex;
|
||||||
});
|
});
|
||||||
}, [sentences]);
|
|
||||||
|
// Reset skip flag after a short delay
|
||||||
|
setTimeout(() => {
|
||||||
|
skipTriggeredRef.current = false;
|
||||||
|
}, 100);
|
||||||
|
}, [sentences, activeSource]);
|
||||||
|
|
||||||
const setText = useCallback((text: string) => {
|
const setText = useCallback((text: string) => {
|
||||||
setCurrentText(text);
|
setCurrentText(text);
|
||||||
const newSentences = splitIntoSentences(text);
|
const newSentences = splitIntoSentences(text);
|
||||||
setSentences(newSentences);
|
setSentences(newSentences);
|
||||||
setCurrentIndex(0);
|
setCurrentIndex(0);
|
||||||
|
setIsPlaying(false);
|
||||||
|
|
||||||
|
// Clear audio cache
|
||||||
|
audioCacheRef.current.clear();
|
||||||
|
|
||||||
|
// Preload the first sentence immediately
|
||||||
if (newSentences.length > 0) {
|
if (newSentences.length > 0) {
|
||||||
console.log('Starting sentence:', newSentences[0]);
|
preloadSentence(newSentences[0]).then(() => {
|
||||||
|
// Preload the second sentence after a small delay
|
||||||
|
if (newSentences[1]) {
|
||||||
|
setTimeout(() => preloadSentence(newSentences[1]), 200);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Preload adjacent sentences when currentIndex changes
|
||||||
|
useEffect(() => {
|
||||||
|
const preloadAdjacentSentences = async () => {
|
||||||
|
try {
|
||||||
|
// Only preload next sentence to reduce API load
|
||||||
|
if (sentences[currentIndex + 1] && !audioCacheRef.current.has(sentences[currentIndex + 1])) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 200)); // Add small delay
|
||||||
|
await preloadSentence(sentences[currentIndex + 1]);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error preloading adjacent sentences:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
preloadAdjacentSentences();
|
||||||
|
}, [currentIndex, sentences]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isPlaying && sentences[currentIndex] && !isProcessing) {
|
||||||
|
processAndPlaySentence(sentences[currentIndex]);
|
||||||
|
}
|
||||||
|
}, [isPlaying, currentIndex, sentences, isProcessing]);
|
||||||
|
|
||||||
|
const preloadSentence = async (sentence: string) => {
|
||||||
|
if (!audioContext) return;
|
||||||
|
if (audioCacheRef.current.has(sentence)) return; // Already cached
|
||||||
|
|
||||||
|
try {
|
||||||
|
console.log(' Preloading TTS for sentence:', sentence.substring(0, 50) + '...');
|
||||||
|
const startTime = Date.now();
|
||||||
|
const response = await openai.audio.speech.create({
|
||||||
|
model: 'tts-1',
|
||||||
|
voice: 'alloy',
|
||||||
|
input: sentence,
|
||||||
|
});
|
||||||
|
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
console.log(` Preload TTS completed in ${duration}ms`);
|
||||||
|
|
||||||
|
const arrayBuffer = await response.arrayBuffer();
|
||||||
|
const audioBuffer = await audioContext.decodeAudioData(arrayBuffer);
|
||||||
|
|
||||||
|
// Store in cache
|
||||||
|
audioCacheRef.current.set(sentence, audioBuffer);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error && error.name === 'AbortError') {
|
||||||
|
console.log('Request was cancelled');
|
||||||
|
} else {
|
||||||
|
console.error('Error preloading TTS:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const value = {
|
const value = {
|
||||||
isPlaying,
|
isPlaying,
|
||||||
currentText,
|
currentText,
|
||||||
|
|
@ -66,6 +318,8 @@ export function TTSProvider({ children }: { children: React.ReactNode }) {
|
||||||
skipBackward,
|
skipBackward,
|
||||||
setText,
|
setText,
|
||||||
currentSentence: sentences[currentIndex] || '',
|
currentSentence: sentences[currentIndex] || '',
|
||||||
|
audioQueue,
|
||||||
|
currentAudioIndex,
|
||||||
};
|
};
|
||||||
|
|
||||||
return <TTSContext.Provider value={value}>{children}</TTSContext.Provider>;
|
return <TTSContext.Provider value={value}>{children}</TTSContext.Provider>;
|
||||||
|
|
@ -77,4 +331,4 @@ export function useTTS() {
|
||||||
throw new Error('useTTS must be used within a TTSProvider');
|
throw new Error('useTTS must be used within a TTSProvider');
|
||||||
}
|
}
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue