added: grade, subject and chapter in new chat, fixed that shitty card bug
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"chrome-devtools": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "chrome-devtools-mcp@latest", "--executablePath", "/usr/bin/brave"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
# AI Provider: "openai" | "anthropic" | "google"
|
||||
AI_PROVIDER=openai
|
||||
|
||||
# OpenAI
|
||||
OPENAI_API_KEY=sk-your-key-here
|
||||
OPENAI_MODEL=gpt-4o
|
||||
|
||||
# Anthropic (optional)
|
||||
ANTHROPIC_API_KEY=sk-ant-your-key-here
|
||||
ANTHROPIC_MODEL=claude-sonnet-4-20250514
|
||||
|
||||
# Google (optional)
|
||||
GOOGLE_API_KEY=your-google-api-key-here
|
||||
GOOGLE_MODEL=gemini-2.0-flash
|
||||
|
||||
# Server
|
||||
PORT=3001
|
||||
CORS_ORIGIN=http://localhost:5173
|
||||
@@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
.env
|
||||
Generated
+927
@@ -0,0 +1,927 @@
|
||||
{
|
||||
"name": "padhle-backend",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "padhle-backend",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.6.0",
|
||||
"express": "^5.1.0",
|
||||
"openai": "^5.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
|
||||
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-types": "^3.0.0",
|
||||
"negotiator": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
|
||||
"integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "^3.1.2",
|
||||
"content-type": "^2.0.0",
|
||||
"debug": "^4.4.3",
|
||||
"http-errors": "^2.0.1",
|
||||
"iconv-lite": "^0.7.2",
|
||||
"on-finished": "^2.4.1",
|
||||
"qs": "^6.15.2",
|
||||
"raw-body": "^3.0.2",
|
||||
"type-is": "^2.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser/node_modules/content-type": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
|
||||
"integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bound": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
|
||||
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"get-intrinsic": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/content-disposition": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.1.0.tgz",
|
||||
"integrity": "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/content-type": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
|
||||
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/cookie-signature": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
|
||||
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cors": {
|
||||
"version": "2.8.6",
|
||||
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
|
||||
"integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"object-assign": "^4",
|
||||
"vary": "^1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/depd": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
|
||||
"integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "16.6.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
|
||||
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/ee-first": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
|
||||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz",
|
||||
"integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/etag": {
|
||||
"version": "1.8.1",
|
||||
"resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
|
||||
"integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.1",
|
||||
"content-disposition": "^1.0.0",
|
||||
"content-type": "^1.0.5",
|
||||
"cookie": "^0.7.1",
|
||||
"cookie-signature": "^1.2.1",
|
||||
"debug": "^4.4.0",
|
||||
"depd": "^2.0.0",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"etag": "^1.8.1",
|
||||
"finalhandler": "^2.1.0",
|
||||
"fresh": "^2.0.0",
|
||||
"http-errors": "^2.0.0",
|
||||
"merge-descriptors": "^2.0.0",
|
||||
"mime-types": "^3.0.0",
|
||||
"on-finished": "^2.4.1",
|
||||
"once": "^1.4.0",
|
||||
"parseurl": "^1.3.3",
|
||||
"proxy-addr": "^2.0.7",
|
||||
"qs": "^6.14.0",
|
||||
"range-parser": "^1.2.1",
|
||||
"router": "^2.2.0",
|
||||
"send": "^1.1.0",
|
||||
"serve-static": "^2.2.0",
|
||||
"statuses": "^2.0.1",
|
||||
"type-is": "^2.0.1",
|
||||
"vary": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
|
||||
"integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.0",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"on-finished": "^2.4.1",
|
||||
"parseurl": "^1.3.3",
|
||||
"statuses": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fresh": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
|
||||
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/http-errors": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
|
||||
"integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"depd": "~2.0.0",
|
||||
"inherits": "~2.0.4",
|
||||
"setprototypeof": "~1.2.0",
|
||||
"statuses": "~2.0.2",
|
||||
"toidentifier": "~1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz",
|
||||
"integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/inherits": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
|
||||
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/ipaddr.js": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||
"integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/is-promise": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
|
||||
"integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/media-typer": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz",
|
||||
"integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
|
||||
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.54.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
|
||||
"integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
|
||||
"integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "^1.54.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"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/negotiator": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
|
||||
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/object-assign": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.4",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
|
||||
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/on-finished": {
|
||||
"version": "2.4.1",
|
||||
"resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
|
||||
"integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ee-first": "1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/once": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
|
||||
"integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/openai": {
|
||||
"version": "5.23.2",
|
||||
"resolved": "https://registry.npmjs.org/openai/-/openai-5.23.2.tgz",
|
||||
"integrity": "sha512-MQBzmTulj+MM5O8SKEk/gL8a7s5mktS9zUtAkU257WjvobGc9nKcBuVwjyEEcb9SI8a8Y2G/mzn3vm9n1Jlleg==",
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"openai": "bin/cli"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"ws": "^8.18.0",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"ws": {
|
||||
"optional": true
|
||||
},
|
||||
"zod": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/parseurl": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
|
||||
"integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "8.4.2",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz",
|
||||
"integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
"integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"forwarded": "0.2.0",
|
||||
"ipaddr.js": "1.9.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.15.3",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
|
||||
"integrity": "sha512-O9gl3zCl5h5blw1KGUzQKhA5oUXSl8rwUIM5o0S3nCXMliSvy5Dzx7/DJcI+SwgICv+IneSZwhBh1oSyEHA71A==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"es-define-property": "^1.0.1",
|
||||
"side-channel": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz",
|
||||
"integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
|
||||
"integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "~3.1.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.7.0",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/router": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
|
||||
"integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.0",
|
||||
"depd": "^2.0.0",
|
||||
"is-promise": "^4.0.0",
|
||||
"parseurl": "^1.3.3",
|
||||
"path-to-regexp": "^8.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
|
||||
"integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "^4.4.3",
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"etag": "^1.8.1",
|
||||
"fresh": "^2.0.0",
|
||||
"http-errors": "^2.0.1",
|
||||
"mime-types": "^3.0.2",
|
||||
"ms": "^2.1.3",
|
||||
"on-finished": "^2.4.1",
|
||||
"range-parser": "^1.2.1",
|
||||
"statuses": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
|
||||
"integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"encodeurl": "^2.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"parseurl": "^1.3.3",
|
||||
"send": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/setprototypeof": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
|
||||
"integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/side-channel": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz",
|
||||
"integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.4",
|
||||
"side-channel-list": "^1.0.1",
|
||||
"side-channel-map": "^1.0.1",
|
||||
"side-channel-weakmap": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-list": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
|
||||
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-map": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
|
||||
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/side-channel-weakmap": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
|
||||
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bound": "^1.0.2",
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.5",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-map": "^1.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/statuses": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
|
||||
"integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/toidentifier": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
|
||||
"integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz",
|
||||
"integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"content-type": "^2.0.0",
|
||||
"media-typer": "^1.1.0",
|
||||
"mime-types": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is/node_modules/content-type": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz",
|
||||
"integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/unpipe": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
|
||||
"integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/vary": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
|
||||
"integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrappy": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
|
||||
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
|
||||
"license": "ISC"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "padhle-backend",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"description": "AI-powered educational chat backend for padhle",
|
||||
"scripts": {
|
||||
"dev": "node --watch src/index.js",
|
||||
"start": "node src/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.1.0",
|
||||
"openai": "^5.11.0",
|
||||
"dotenv": "^16.6.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import dotenv from "dotenv";
|
||||
import cors from "cors";
|
||||
import express from "express";
|
||||
import chatRoutes from "./routes/chat.js";
|
||||
import sessionRoutes from "./routes/sessions.js";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3001;
|
||||
const CORS_ORIGIN = process.env.CORS_ORIGIN || "http://localhost:5173";
|
||||
|
||||
// Middleware
|
||||
app.use(cors({ origin: CORS_ORIGIN, credentials: true }));
|
||||
app.use(express.json());
|
||||
|
||||
// Health check
|
||||
app.get("/health", (_req, res) => {
|
||||
res.json({ status: "ok", provider: process.env.AI_PROVIDER });
|
||||
});
|
||||
|
||||
// Routes
|
||||
app.use("/api/chat", chatRoutes);
|
||||
app.use("/api/sessions", sessionRoutes);
|
||||
|
||||
// 404 handler
|
||||
app.use((_req, res) => {
|
||||
res.status(404).json({ error: "Endpoint not found" });
|
||||
});
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`\n 📚 padhle backend running on http://localhost:${PORT}`);
|
||||
console.log(` Provider: ${process.env.AI_PROVIDER || "openai"}`);
|
||||
console.log(` Frontend: ${CORS_ORIGIN}\n`);
|
||||
});
|
||||
@@ -0,0 +1,132 @@
|
||||
import express from "express";
|
||||
import { streamChatResponse } from "../services/ai.js";
|
||||
import { createSession, addMessage, getMessages, getSession, listSessions } from "../stores/sessionStore.js";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* POST /api/chat — Send a message and stream back an AI response.
|
||||
*
|
||||
* Request body:
|
||||
* {
|
||||
* text: "What is photosynthesis?",
|
||||
* chatId: "optional_existing_session_id",
|
||||
* grade: "Grade 10",
|
||||
* subject: "Biology",
|
||||
* chapter: "Nutrition in Plants"
|
||||
* }
|
||||
*/
|
||||
router.post("/", async (req, res) => {
|
||||
const { text, chatId, grade, subject, chapter } = req.body;
|
||||
|
||||
// Validate input
|
||||
if (!text || !text.trim()) {
|
||||
return res.status(400).json({ error: "Message text is required" });
|
||||
}
|
||||
|
||||
// Resolve or create session
|
||||
let session;
|
||||
if (chatId) {
|
||||
session = getSession(chatId);
|
||||
if (!session) {
|
||||
return res.status(404).json({ error: "Session not found" });
|
||||
}
|
||||
} else {
|
||||
session = createSession(grade, subject, chapter);
|
||||
}
|
||||
|
||||
const currentChatId = session.id;
|
||||
|
||||
// Get conversation history
|
||||
const chatMessages = getMessages(currentChatId);
|
||||
|
||||
// Add user message to history
|
||||
const userMsg = {
|
||||
id: `user_${Date.now()}`,
|
||||
role: "user",
|
||||
text: text.trim(),
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
addMessage(currentChatId, userMsg);
|
||||
|
||||
// Set SSE headers
|
||||
res.setHeader("Content-Type", "text/event-stream");
|
||||
res.setHeader("Cache-Control", "no-cache");
|
||||
res.setHeader("Connection", "keep-alive");
|
||||
res.setHeader("X-Accel-Buffering", "no");
|
||||
|
||||
let assistantText = "";
|
||||
let errorOccurred = false;
|
||||
|
||||
// Stream the AI response
|
||||
try {
|
||||
await streamChatResponse(
|
||||
chatMessages,
|
||||
grade || session.grade,
|
||||
subject || session.subject,
|
||||
chapter || session.chapter,
|
||||
(chunk) => {
|
||||
assistantText += chunk;
|
||||
// Send chunk as SSE event
|
||||
res.write(`data: ${JSON.stringify({ type: "chunk", content: chunk })}\n\n`);
|
||||
},
|
||||
(err) => {
|
||||
errorOccurred = true;
|
||||
res.write(`data: ${JSON.stringify({ type: "error", message: err })}\n\n`);
|
||||
res.end();
|
||||
}
|
||||
);
|
||||
|
||||
// Send end event
|
||||
res.write(`data: ${JSON.stringify({ type: "done" })}\n\n`);
|
||||
|
||||
// Save assistant message to history
|
||||
const assistantMsg = {
|
||||
id: `assistant_${Date.now()}`,
|
||||
role: "assistant",
|
||||
text: assistantText,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
addMessage(currentChatId, assistantMsg);
|
||||
} catch (err) {
|
||||
res.write(`data: ${JSON.stringify({ type: "error", message: err.message })}\n\n`);
|
||||
} finally {
|
||||
res.end();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/chat/:chatId — Get conversation messages for a session
|
||||
*/
|
||||
router.get("/:chatId", (req, res) => {
|
||||
const { chatId } = req.params;
|
||||
const session = getSession(chatId);
|
||||
|
||||
if (!session) {
|
||||
return res.status(404).json({ error: "Session not found" });
|
||||
}
|
||||
|
||||
const msgHistory = getMessages(chatId);
|
||||
|
||||
res.json({
|
||||
session: {
|
||||
id: session.id,
|
||||
grade: session.grade,
|
||||
subject: session.subject,
|
||||
chapter: session.chapter,
|
||||
createdAt: session.createdAt,
|
||||
updatedAt: session.updatedAt,
|
||||
},
|
||||
messages: msgHistory,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/chat/sessions — List all chat sessions
|
||||
*/
|
||||
router.get("/sessions", (_req, res) => {
|
||||
const sessions = listSessions();
|
||||
res.json(sessions);
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,58 @@
|
||||
import express from "express";
|
||||
import { listSessions, getSession, deleteSession, clearMessages, createSession } from "../stores/sessionStore.js";
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
/**
|
||||
* GET /api/sessions — List all sessions
|
||||
*/
|
||||
router.get("/", (_req, res) => {
|
||||
const sessions = listSessions();
|
||||
res.json(sessions);
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/sessions — Create a new session
|
||||
*/
|
||||
router.post("/", (req, res) => {
|
||||
const { grade, subject, chapter } = req.body;
|
||||
const session = createSession(grade, subject, chapter);
|
||||
res.status(201).json(session);
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/sessions/:id — Get a single session
|
||||
*/
|
||||
router.get("/:id", (req, res) => {
|
||||
const session = getSession(req.params.id);
|
||||
if (!session) {
|
||||
return res.status(404).json({ error: "Session not found" });
|
||||
}
|
||||
res.json(session);
|
||||
});
|
||||
|
||||
/**
|
||||
* DELETE /api/sessions/:id — Delete a session
|
||||
*/
|
||||
router.delete("/:id", (req, res) => {
|
||||
const deleted = deleteSession(req.params.id);
|
||||
if (!deleted) {
|
||||
return res.status(404).json({ error: "Session not found" });
|
||||
}
|
||||
res.json({ message: "Session deleted", id: req.params.id });
|
||||
});
|
||||
|
||||
/**
|
||||
* PATCH /api/sessions/:id/clear — Clear messages but keep session
|
||||
*/
|
||||
router.patch("/:id/clear", (req, res) => {
|
||||
const { id } = req.params;
|
||||
const session = getSession(id);
|
||||
if (!session) {
|
||||
return res.status(404).json({ error: "Session not found" });
|
||||
}
|
||||
clearMessages(id);
|
||||
res.json({ message: "Messages cleared", id });
|
||||
});
|
||||
|
||||
export default router;
|
||||
@@ -0,0 +1,295 @@
|
||||
import OpenAI from "openai";
|
||||
import dotenv from "dotenv";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
const provider = process.env.AI_PROVIDER || "openai";
|
||||
|
||||
// --- OpenAI client ---
|
||||
const openai = new OpenAI({
|
||||
apiKey: process.env.OPENAI_API_KEY,
|
||||
});
|
||||
|
||||
// --- Anthropic (via OpenAI SDK compatible endpoint or raw) ---
|
||||
const ANTHROPIC_BASE_URL = "https://api.anthropic.com";
|
||||
|
||||
// --- Google (via OpenAI SDK compatible endpoint) ---
|
||||
const GOOGLE_BASE_URL = "https://generativelanguage.googleapis.com/v1beta/openai/";
|
||||
|
||||
/**
|
||||
* Build the system prompt based on grade/subject/chapter context.
|
||||
*/
|
||||
function buildSystemPrompt(grade, subject, chapter) {
|
||||
const context = [];
|
||||
|
||||
if (grade && grade !== "General") context.push(`You are teaching students in grade ${grade}.`);
|
||||
if (subject && subject !== "General") context.push(`The subject is ${subject}.`);
|
||||
if (chapter && chapter !== "General") context.push(`The relevant chapter is ${chapter}.`);
|
||||
|
||||
context.push(
|
||||
"You are padhle, an AI tutor designed to help students learn.",
|
||||
"Be clear, patient, and encouraging. Use simple language appropriate for the student's grade level.",
|
||||
"When explaining concepts, use examples, analogies, and step-by-step breakdowns.",
|
||||
"If a question is outside the selected subject/chapter, politely redirect to the topic or answer briefly while noting the scope.",
|
||||
"Never just give answers for homework — explain the reasoning so the student learns.",
|
||||
"Format your responses using markdown: headings for structure, bullet points for lists, and code blocks for examples."
|
||||
);
|
||||
|
||||
return context.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the messages array for the AI call.
|
||||
*/
|
||||
function buildMessages(chatMessages, grade, subject, chapter) {
|
||||
const systemMsg = {
|
||||
role: "system",
|
||||
content: buildSystemPrompt(grade, subject, chapter),
|
||||
};
|
||||
|
||||
const userMessages = chatMessages.map((m) => ({
|
||||
role: m.role === "user" ? "user" : "assistant",
|
||||
content: m.text,
|
||||
}));
|
||||
|
||||
return [systemMsg, ...userMessages];
|
||||
}
|
||||
|
||||
/**
|
||||
* Call OpenAI and stream the response via a callback.
|
||||
*/
|
||||
async function streamOpenai(messages, onChunk, onError) {
|
||||
const stream = await openai.chat.completions.create({
|
||||
model: process.env.OPENAI_MODEL || "gpt-4o",
|
||||
messages,
|
||||
stream: true,
|
||||
max_tokens: 2048,
|
||||
});
|
||||
|
||||
let fullResponse = "";
|
||||
for await (const chunk of stream) {
|
||||
const content = chunk.choices[0]?.delta?.content || "";
|
||||
if (content) {
|
||||
fullResponse += content;
|
||||
onChunk(content);
|
||||
}
|
||||
}
|
||||
|
||||
return fullResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call Anthropic and stream the response.
|
||||
*/
|
||||
async function streamAnthropic(messages, onChunk, onError) {
|
||||
const userMsgs = messages.filter((m) => m.role === "user");
|
||||
const systemMsg = messages.find((m) => m.role === "system");
|
||||
|
||||
const body = {
|
||||
model: process.env.ANTHROPIC_MODEL || "claude-sonnet-4-20250514",
|
||||
max_tokens: 2048,
|
||||
system: systemMsg?.content || "",
|
||||
messages: userMsgs.map((m) => ({ role: "user", content: m.content })),
|
||||
stream: true,
|
||||
};
|
||||
|
||||
const response = await fetch(ANTHROPIC_BASE_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": process.env.ANTHROPIC_API_KEY,
|
||||
"anthropic-version": "2023-06-01",
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const err = await response.text();
|
||||
onError(`Anthropic API error: ${err}`);
|
||||
return "";
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let fullResponse = "";
|
||||
let buffer = "";
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() || "";
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed.startsWith("data: ")) continue;
|
||||
const jsonStr = trimmed.slice(6);
|
||||
if (jsonStr === "[DONE]") continue;
|
||||
|
||||
try {
|
||||
const json = JSON.parse(jsonStr);
|
||||
const content = json.delta?.text || "";
|
||||
if (content) {
|
||||
fullResponse += content;
|
||||
onChunk(content);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
return fullResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call Google Gemini and stream the response.
|
||||
*/
|
||||
async function streamGoogle(messages, onChunk, onError) {
|
||||
const model = process.env.GOOGLE_MODEL || "gemini-2.0-flash";
|
||||
const apiKey = process.env.GOOGLE_API_KEY;
|
||||
const url = `${GOOGLE_BASE_URL}${apiKey}/chat/models/${model}:streamGenerateContent?key=${apiKey}`;
|
||||
|
||||
const systemMsg = messages.find((m) => m.role === "system");
|
||||
const userMsgs = messages.filter((m) => m.role !== "system");
|
||||
|
||||
// Convert messages to Gemini format
|
||||
const contents = userMsgs.map((m) => ({
|
||||
role: m.role === "user" ? "user" : "model",
|
||||
parts: [{ text: m.content }],
|
||||
}));
|
||||
|
||||
const body = {
|
||||
contents,
|
||||
systemInstruction: systemMsg ? { parts: [{ text: systemMsg.content }] } : undefined,
|
||||
generationConfig: { maxOutputTokens: 2048 },
|
||||
};
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const err = await response.text();
|
||||
onError(`Google AI error: ${err}`);
|
||||
return "";
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let fullResponse = "";
|
||||
let buffer = "";
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() || "";
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
|
||||
try {
|
||||
const json = JSON.parse(trimmed);
|
||||
const candidates = json.candidates?.[0];
|
||||
const parts = candidates?.content?.parts;
|
||||
const text = parts?.[0]?.text || "";
|
||||
if (text) {
|
||||
fullResponse += text;
|
||||
onChunk(text);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
|
||||
return fullResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stream a chat response.
|
||||
* @param {Array} chatMessages - Array of {role, text} messages
|
||||
* @param {string} grade
|
||||
* @param {string} subject
|
||||
* @param {string} chapter
|
||||
* @param {Function} onChunk - Called with each text chunk
|
||||
* @param {Function} onError - Called on error
|
||||
* @returns {Promise<string>} The full response text
|
||||
*/
|
||||
export async function streamChatResponse(chatMessages, grade, subject, chapter, onChunk, onError) {
|
||||
const messages = buildMessages(chatMessages, grade, subject, chapter);
|
||||
|
||||
switch (provider) {
|
||||
case "anthropic":
|
||||
return streamAnthropic(messages, onChunk, onError);
|
||||
case "google":
|
||||
return streamGoogle(messages, onChunk, onError);
|
||||
case "openai":
|
||||
default:
|
||||
return streamOpenai(messages, onChunk, onError);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a non-streaming response (fallback for simple queries).
|
||||
*/
|
||||
export async function getChatResponse(chatMessages, grade, subject, chapter) {
|
||||
const messages = buildMessages(chatMessages, grade, subject, chapter);
|
||||
|
||||
let response;
|
||||
switch (provider) {
|
||||
case "anthropic": {
|
||||
const userMsgs = messages.filter((m) => m.role === "user");
|
||||
const systemMsg = messages.find((m) => m.role === "system");
|
||||
const res = await fetch(ANTHROPIC_BASE_URL, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": process.env.ANTHROPIC_API_KEY,
|
||||
"anthropic-version": "2023-06-01",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
model: process.env.ANTHROPIC_MODEL || "claude-sonnet-4-20250514",
|
||||
max_tokens: 2048,
|
||||
system: systemMsg?.content || "",
|
||||
messages: userMsgs.map((m) => ({ role: "user", content: m.content })),
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
return data.content?.[0]?.text || "No response";
|
||||
}
|
||||
case "google": {
|
||||
const systemMsg = messages.find((m) => m.role === "system");
|
||||
const userMsgs = messages.filter((m) => m.role !== "system");
|
||||
const contents = userMsgs.map((m) => ({
|
||||
role: m.role === "user" ? "user" : "model",
|
||||
parts: [{ text: m.content }],
|
||||
}));
|
||||
const url = `${GOOGLE_BASE_URL}${process.env.GOOGLE_API_KEY}/chat/models/${process.env.GOOGLE_MODEL || "gemini-2.0-flash"}:generateContent?key=${process.env.GOOGLE_API_KEY}`;
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
contents,
|
||||
systemInstruction: systemMsg ? { parts: [{ text: systemMsg.content }] } : undefined,
|
||||
generationConfig: { maxOutputTokens: 2048 },
|
||||
}),
|
||||
});
|
||||
const data = await res.json();
|
||||
return data.candidates?.[0]?.content?.parts?.[0]?.text || "No response";
|
||||
}
|
||||
case "openai":
|
||||
default: {
|
||||
const res = await openai.chat.completions.create({
|
||||
model: process.env.OPENAI_MODEL || "gpt-4o",
|
||||
messages,
|
||||
max_tokens: 2048,
|
||||
});
|
||||
return res.choices?.[0]?.message?.content || "No response";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/**
|
||||
* In-memory session store.
|
||||
* Each session holds a chatId, metadata (grade/subject/chapter),
|
||||
* and a list of messages. Swappable for a DB backend later.
|
||||
*/
|
||||
|
||||
const sessions = new Map();
|
||||
const messages = new Map(); // chatId -> [Message]
|
||||
|
||||
function generateId() {
|
||||
return `chat_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new chat session
|
||||
*/
|
||||
export function createSession(grade, subject, chapter) {
|
||||
const chatId = generateId();
|
||||
const session = {
|
||||
id: chatId,
|
||||
grade: grade || "General",
|
||||
subject: subject || "General",
|
||||
chapter: chapter || "General",
|
||||
preview: "New Chat",
|
||||
createdAt: new Date().toISOString(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
messageCount: 0,
|
||||
};
|
||||
|
||||
sessions.set(chatId, session);
|
||||
messages.set(chatId, []);
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a session by ID
|
||||
*/
|
||||
export function getSession(chatId) {
|
||||
return sessions.get(chatId) || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all sessions (most recent first)
|
||||
*/
|
||||
export function listSessions() {
|
||||
const all = Array.from(sessions.values());
|
||||
return all.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get messages for a chat
|
||||
*/
|
||||
export function getMessages(chatId) {
|
||||
return messages.get(chatId) || [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a message to a chat session
|
||||
*/
|
||||
export function addMessage(chatId, message) {
|
||||
const msgs = messages.get(chatId);
|
||||
if (msgs) {
|
||||
msgs.push(message);
|
||||
}
|
||||
|
||||
// Update session metadata
|
||||
const session = sessions.get(chatId);
|
||||
if (session) {
|
||||
session.updatedAt = new Date().toISOString();
|
||||
session.messageCount = msgs ? msgs.length : 0;
|
||||
|
||||
// Set preview from the first user message
|
||||
if (session.messageCount <= 1 && message.role === "user") {
|
||||
session.preview = message.text.substring(0, 60) + (message.text.length > 60 ? "..." : "");
|
||||
}
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a session and its messages
|
||||
*/
|
||||
export function deleteSession(chatId) {
|
||||
const deleted = sessions.delete(chatId);
|
||||
messages.delete(chatId);
|
||||
return deleted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all messages for a session (keep session metadata)
|
||||
*/
|
||||
export function clearMessages(chatId) {
|
||||
messages.set(chatId, []);
|
||||
const session = sessions.get(chatId);
|
||||
if (session) {
|
||||
session.messageCount = 0;
|
||||
session.updatedAt = new Date().toISOString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get stats (for monitoring)
|
||||
*/
|
||||
export function getStats() {
|
||||
return {
|
||||
activeSessions: sessions.size,
|
||||
totalMessages: Array.from(messages.values()).reduce(
|
||||
(sum, msgs) => sum + msgs.length,
|
||||
0
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -27,3 +27,6 @@ build
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
|
||||
# LLM memory / documentation
|
||||
markdown/
|
||||
|
||||
+273
-16
@@ -1,48 +1,299 @@
|
||||
/* ========================================
|
||||
App Component — padhle (Refined Workspace)
|
||||
App Component — padhle (Backend-Connected)
|
||||
======================================== */
|
||||
|
||||
import { useState } from "react";
|
||||
import { useState, useRef, useCallback, useEffect } from "react";
|
||||
import Sidebar from "./components/sidebar/Sidebar";
|
||||
import TopNav from "./components/top-nav/TopNav";
|
||||
import ChatHistory from "./components/chat-history/ChatHistory";
|
||||
import ChatInput from "./components/chat-input/ChatInput";
|
||||
import SelectorFlow from "./components/selector-flow/SelectorFlow";
|
||||
|
||||
const API_BASE = ""; // Uses Vite proxy → localhost:3001 in dev
|
||||
|
||||
// Default selectors
|
||||
const DEFAULTS = {
|
||||
grade: "Choose standard",
|
||||
subject: "Choose Subject",
|
||||
chapter: "Choose Chapter",
|
||||
};
|
||||
|
||||
function App() {
|
||||
const [messages, setMessages] = useState([]);
|
||||
const [activeSubject, setActiveSubject] = useState("Choose Subject");
|
||||
const [activeSubject, setActiveSubject] = useState(DEFAULTS.subject);
|
||||
const [activeChat, setActiveChat] = useState(null);
|
||||
const [activeGrade, setActiveGrade] = useState("Choose standard");
|
||||
const [activeChapter, setActiveChapter] = useState("Choose Chapter");
|
||||
const [activeGrade, setActiveGrade] = useState(DEFAULTS.grade);
|
||||
const [activeChapter, setActiveChapter] = useState(DEFAULTS.chapter);
|
||||
const [isTyping, setIsTyping] = useState(false);
|
||||
const [sessions, setSessions] = useState([]);
|
||||
const [isLoadingSessions, setIsLoadingSessions] = useState(false);
|
||||
const [showSelectorFlow, setShowSelectorFlow] = useState(false);
|
||||
const abortRef = useRef(null);
|
||||
|
||||
const handleSend = (text) => {
|
||||
if (!text.trim()) return;
|
||||
// Load sessions on mount
|
||||
useEffect(() => {
|
||||
loadSessions();
|
||||
}, []);
|
||||
|
||||
setMessages((prev) => [...prev, { id: Date.now(), role: "user", text }]);
|
||||
const loadSessions = async () => {
|
||||
setIsLoadingSessions(true);
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/sessions`);
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
setSessions(data);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to load sessions:", err);
|
||||
} finally {
|
||||
setIsLoadingSessions(false);
|
||||
}
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
const handleNewChat = () => {
|
||||
// Abort any in-flight request
|
||||
if (abortRef.current) {
|
||||
abortRef.current.abort();
|
||||
abortRef.current = null;
|
||||
}
|
||||
|
||||
// Clear everything
|
||||
setMessages([]);
|
||||
setActiveChat(null);
|
||||
setActiveGrade(DEFAULTS.grade);
|
||||
setActiveSubject(DEFAULTS.subject);
|
||||
setActiveChapter(DEFAULTS.chapter);
|
||||
setIsTyping(false);
|
||||
|
||||
// Show the selector flow
|
||||
setShowSelectorFlow(true);
|
||||
|
||||
// Refresh sessions list from backend
|
||||
loadSessions();
|
||||
};
|
||||
|
||||
const handleChatSelect = async (sessionId) => {
|
||||
// Abort any in-flight request
|
||||
if (abortRef.current) {
|
||||
abortRef.current.abort();
|
||||
abortRef.current = null;
|
||||
}
|
||||
|
||||
setIsTyping(true);
|
||||
setMessages([]);
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/chat/${sessionId}`);
|
||||
if (!res.ok) {
|
||||
throw new Error("Session not found");
|
||||
}
|
||||
const data = await res.json();
|
||||
|
||||
// Set the session's context (grade, subject, chapter)
|
||||
if (data.session) {
|
||||
setActiveGrade(data.session.grade || DEFAULTS.grade);
|
||||
setActiveSubject(data.session.subject || DEFAULTS.subject);
|
||||
setActiveChapter(data.session.chapter || DEFAULTS.chapter);
|
||||
}
|
||||
|
||||
// Hide selector flow when loading an existing chat
|
||||
setShowSelectorFlow(false);
|
||||
|
||||
// Load the messages
|
||||
if (data.messages) {
|
||||
setMessages(data.messages.map((m) => ({ ...m, streaming: false })));
|
||||
}
|
||||
|
||||
setActiveChat(sessionId);
|
||||
} catch (err) {
|
||||
console.error("Failed to load session:", err);
|
||||
setMessages([
|
||||
{
|
||||
id: Date.now(),
|
||||
role: "assistant",
|
||||
text: `⚠️ Could not load that chat. It may have been deleted.`,
|
||||
},
|
||||
]);
|
||||
} finally {
|
||||
setIsTyping(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteChat = async (sessionId) => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/sessions/${sessionId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
if (res.ok) {
|
||||
// If we were on the deleted chat, clear it
|
||||
if (activeChat === sessionId) {
|
||||
handleNewChat();
|
||||
} else {
|
||||
loadSessions();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to delete session:", err);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSend = useCallback(
|
||||
(text) => {
|
||||
if (!text.trim() || isTyping) return;
|
||||
|
||||
// Abort any previous in-flight request
|
||||
if (abortRef.current) {
|
||||
abortRef.current.abort();
|
||||
}
|
||||
|
||||
// Add user message immediately
|
||||
const userMsg = { id: Date.now(), role: "user", text };
|
||||
setMessages((prev) => [...prev, userMsg]);
|
||||
setIsTyping(true);
|
||||
|
||||
const fetchChatId = activeChat;
|
||||
|
||||
// Build request body
|
||||
const body = {
|
||||
text: text.trim(),
|
||||
chatId: fetchChatId,
|
||||
grade: activeGrade !== DEFAULTS.grade ? activeGrade : undefined,
|
||||
subject: activeSubject !== DEFAULTS.subject ? activeSubject : undefined,
|
||||
chapter: activeChapter !== DEFAULTS.chapter ? activeChapter : undefined,
|
||||
};
|
||||
|
||||
// Open SSE connection
|
||||
const controller = new AbortController();
|
||||
abortRef.current = controller;
|
||||
|
||||
fetch(`${API_BASE}/api/chat`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
signal: controller.signal,
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Server error: ${response.status}`);
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
let assistantText = "";
|
||||
|
||||
// Create placeholder assistant message
|
||||
const assistantId = Date.now() + 1;
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{ id: assistantId, role: "assistant", text: "", streaming: true },
|
||||
]);
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() || "";
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith("data: ")) continue;
|
||||
const jsonStr = line.slice(6);
|
||||
if (jsonStr === "[DONE]") continue;
|
||||
|
||||
try {
|
||||
const data = JSON.parse(jsonStr);
|
||||
if (data.type === "chunk") {
|
||||
assistantText += data.content;
|
||||
setMessages((prev) =>
|
||||
prev.map((m) =>
|
||||
m.id === assistantId
|
||||
? { ...m, text: assistantText }
|
||||
: m
|
||||
)
|
||||
);
|
||||
} else if (data.type === "done") {
|
||||
// Mark as non-streaming
|
||||
setMessages((prev) =>
|
||||
prev.map((m) =>
|
||||
m.id === assistantId
|
||||
? { ...m, streaming: false }
|
||||
: m
|
||||
)
|
||||
);
|
||||
} else if (data.type === "error") {
|
||||
setMessages((prev) =>
|
||||
prev.map((m) =>
|
||||
m.id === assistantId
|
||||
? {
|
||||
...m,
|
||||
text: `⚠️ Error: ${data.message}`,
|
||||
streaming: false,
|
||||
}
|
||||
: m
|
||||
)
|
||||
);
|
||||
}
|
||||
} catch {
|
||||
// Skip malformed SSE frames
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
setIsTyping(false);
|
||||
abortRef.current = null;
|
||||
|
||||
// Refresh sessions to update timestamps
|
||||
loadSessions();
|
||||
})
|
||||
.catch((err) => {
|
||||
if (err.name === "AbortError") return; // intentional abort
|
||||
setIsTyping(false);
|
||||
setMessages((prev) => [
|
||||
...prev,
|
||||
{
|
||||
id: Date.now() + 1,
|
||||
role: "assistant",
|
||||
text: "This is a simulated response. Connect the backend to get real AI answers!",
|
||||
text: `⚠️ Connection error: ${err.message}. Make sure the backend is running on port 3001.`,
|
||||
},
|
||||
]);
|
||||
}, 1000);
|
||||
abortRef.current = null;
|
||||
});
|
||||
},
|
||||
[isTyping, activeChat, activeGrade, activeSubject, activeChapter]
|
||||
);
|
||||
|
||||
const handlePromptClick = useCallback(
|
||||
(promptText) => {
|
||||
handleSend(promptText);
|
||||
},
|
||||
[handleSend]
|
||||
);
|
||||
|
||||
const handleGradeChange = (gradeLabel) => {
|
||||
setActiveGrade(gradeLabel);
|
||||
};
|
||||
|
||||
const handlePromptClick = (promptText) => {
|
||||
handleSend(promptText);
|
||||
const handleSubjectChange = (subjectId) => {
|
||||
setActiveSubject(subjectId);
|
||||
};
|
||||
|
||||
const handleChapterChange = (chapterLabel) => {
|
||||
setActiveChapter(chapterLabel);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<Sidebar
|
||||
activeSubject={activeSubject}
|
||||
onSubjectChange={setActiveSubject}
|
||||
onSubjectChange={handleSubjectChange}
|
||||
activeChat={activeChat}
|
||||
onChatSelect={setActiveChat}
|
||||
onChatSelect={handleChatSelect}
|
||||
onNewChat={handleNewChat}
|
||||
sessions={sessions}
|
||||
onDeleteChat={handleDeleteChat}
|
||||
isLoadingSessions={isLoadingSessions}
|
||||
/>
|
||||
|
||||
<div className="page__main">
|
||||
@@ -50,7 +301,7 @@ function App() {
|
||||
activeGrade={activeGrade}
|
||||
onGradeChange={setActiveGrade}
|
||||
activeSubject={activeSubject}
|
||||
onSubjectChange={setActiveSubject}
|
||||
onSubjectChange={handleSubjectChange}
|
||||
activeChapter={activeChapter}
|
||||
onChapterChange={setActiveChapter}
|
||||
/>
|
||||
@@ -62,12 +313,18 @@ function App() {
|
||||
activeGrade={activeGrade}
|
||||
activeChapter={activeChapter}
|
||||
onPromptClick={handlePromptClick}
|
||||
isTyping={isTyping}
|
||||
showSelectorFlow={showSelectorFlow}
|
||||
onGradeChange={handleGradeChange}
|
||||
onSubjectChange={handleSubjectChange}
|
||||
onChapterChange={handleChapterChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ChatInput
|
||||
onSend={handleSend}
|
||||
placeholder={`Ask anything about ${activeGrade} ${activeSubject} – ${activeChapter}...`}
|
||||
disabled={isTyping}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3,9 +3,17 @@
|
||||
======================================== */
|
||||
|
||||
import Message from "../message/Message";
|
||||
import SelectorFlow from "../selector-flow/SelectorFlow";
|
||||
import "./ChatHistory.css";
|
||||
|
||||
const ChatHistory = ({ messages, activeSubject, activeGrade, activeChapter, onPromptClick }) => {
|
||||
const ChatHistory = ({
|
||||
messages,
|
||||
activeGrade,
|
||||
onPromptClick,
|
||||
isTyping,
|
||||
showSelectorFlow,
|
||||
onGradeChange,
|
||||
}) => {
|
||||
const hasMessages = messages.length > 0;
|
||||
|
||||
const suggestions = [
|
||||
@@ -53,6 +61,8 @@ const ChatHistory = ({ messages, activeSubject, activeGrade, activeChapter, onPr
|
||||
|
||||
const fullPromptTexts = suggestions.map((s) => s.text + s.keywords.join(" ") + s.textEnd);
|
||||
|
||||
const streamingMsg = messages.find((m) => m.streaming);
|
||||
|
||||
if (hasMessages) {
|
||||
return (
|
||||
<div className="chat-history">
|
||||
@@ -60,11 +70,23 @@ const ChatHistory = ({ messages, activeSubject, activeGrade, activeChapter, onPr
|
||||
{messages.map((message) => (
|
||||
<Message key={message.id} message={message} />
|
||||
))}
|
||||
{isTyping && !streamingMsg && (
|
||||
<Message message={{ id: "typing", role: "assistant", text: "", streaming: true }} />
|
||||
)}
|
||||
<div ref={(el) => el && el.scrollIntoView({ behavior: "smooth", block: "end" })} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (showSelectorFlow) {
|
||||
return (
|
||||
<div className="chat-history">
|
||||
<SelectorFlow grade={activeGrade} onGradeChange={onGradeChange} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="chat-history">
|
||||
<div className="chat-history__focus">
|
||||
|
||||
@@ -88,6 +88,11 @@
|
||||
font-weight: var(--font-weight-regular);
|
||||
}
|
||||
|
||||
.chat-input__field:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Actions Container */
|
||||
.chat-input__actions {
|
||||
display: flex;
|
||||
@@ -150,6 +155,11 @@
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.chat-input__send-btn:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.chat-input__send-btn .material-symbols-outlined {
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import { useState, useRef, useEffect } from "react";
|
||||
import "./ChatInput.css";
|
||||
|
||||
const ChatInput = ({ onSend, placeholder }) => {
|
||||
const ChatInput = ({ onSend, placeholder, disabled }) => {
|
||||
const [text, setText] = useState("");
|
||||
const textareaRef = useRef(null);
|
||||
|
||||
@@ -75,9 +75,9 @@ const ChatInput = ({ onSend, placeholder }) => {
|
||||
|
||||
{/* Send */}
|
||||
<button
|
||||
className={`chat-input__send-btn ${text.trim() ? "chat-input__send-btn--active" : ""}`}
|
||||
className={`chat-input__send-btn ${text.trim() && !disabled ? "chat-input__send-btn--active" : ""}`}
|
||||
type="submit"
|
||||
disabled={!text.trim()}
|
||||
disabled={!text.trim() || disabled}
|
||||
aria-label="Send message"
|
||||
>
|
||||
<span className="material-symbols-outlined" style={{ fontWeight: text.trim() ? 700 : 200 }}>
|
||||
|
||||
@@ -131,3 +131,43 @@
|
||||
.message__action--primary:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* --- TYPING INDICATOR --- */
|
||||
.message__typing-dots {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-left: 4px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.message__typing-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-primary);
|
||||
animation: typingBounce 1.4s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.message__typing-dot:nth-child(1) {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.message__typing-dot:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.message__typing-dot:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
@keyframes typingBounce {
|
||||
0%, 80%, 100% {
|
||||
transform: scale(0.6);
|
||||
opacity: 0.4;
|
||||
}
|
||||
40% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,20 @@
|
||||
Message Component — padhle (Refined Workspace)
|
||||
======================================== */
|
||||
|
||||
import { useEffect, useRef } from "react";
|
||||
import "./Message.css";
|
||||
|
||||
const Message = ({ message }) => {
|
||||
const { role, text } = message;
|
||||
const { role, text, streaming } = message;
|
||||
const isUser = role === "user";
|
||||
const endRef = useRef(null);
|
||||
|
||||
// Auto-scroll to bottom when text updates
|
||||
useEffect(() => {
|
||||
if (endRef.current) {
|
||||
endRef.current.scrollIntoView({ behavior: "smooth", block: "end" });
|
||||
}
|
||||
}, [text, streaming]);
|
||||
|
||||
return (
|
||||
<div className={`message ${isUser ? "message--user" : "message--assistant"}`}>
|
||||
@@ -18,11 +27,18 @@ const Message = ({ message }) => {
|
||||
</div>
|
||||
)}
|
||||
<div className="message__bubble">
|
||||
<div className="message__text">{text}</div>
|
||||
<div className="message__text">
|
||||
{text || (streaming ? "" : null)}
|
||||
{streaming && <span className="message__typing-dots">
|
||||
<span className="message__typing-dot"></span>
|
||||
<span className="message__typing-dot"></span>
|
||||
<span className="message__typing-dot"></span>
|
||||
</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Action buttons for assistant messages */}
|
||||
{!isUser && (
|
||||
{!isUser && text && (
|
||||
<div className="message__actions">
|
||||
<button className="message__action" aria-label="Helpful" title="Helpful">
|
||||
<span className="material-symbols-outlined">thumb_up</span>
|
||||
@@ -35,6 +51,7 @@ const Message = ({ message }) => {
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div ref={endRef} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
.selector-flow {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 48px 24px;
|
||||
width: 100%;
|
||||
min-height: 320px;
|
||||
}
|
||||
|
||||
.selector-flow__content {
|
||||
width: 100%;
|
||||
max-width: 640px;
|
||||
}
|
||||
|
||||
.selector-flow__title {
|
||||
font-family: var(--font-heading);
|
||||
font-size: 24px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-on-surface);
|
||||
text-align: center;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.selector-flow__subtitle {
|
||||
font-size: 14px;
|
||||
font-weight: var(--font-weight-regular);
|
||||
color: var(--color-on-surface-variant);
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.selector-flow__options {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.selector-flow__option {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 20px 12px;
|
||||
border: var(--border-thin);
|
||||
border-radius: var(--radius-md);
|
||||
background-color: var(--color-surface);
|
||||
cursor: pointer;
|
||||
font-family: var(--font-body);
|
||||
font-size: 14px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--color-on-surface);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.selector-flow__option:hover:not(.selector-flow__option--selected) {
|
||||
background-color: var(--color-surface-low);
|
||||
border-color: var(--color-outline);
|
||||
}
|
||||
|
||||
.selector-flow__option--selected {
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-on-primary);
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.selector-flow__option--selected:hover {
|
||||
background-color: var(--color-primary);
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: var(--shadow-card-hover);
|
||||
}
|
||||
|
||||
.selector-flow__icon {
|
||||
font-size: 28px;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.selector-flow__option--selected .selector-flow__icon {
|
||||
color: var(--color-on-primary);
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useState } from "react";
|
||||
import "./SelectorFlow.css";
|
||||
|
||||
const grades = ["Grade 6", "Grade 7", "Grade 8", "Grade 9", "Grade 10", "Grade 11", "Grade 12"];
|
||||
|
||||
export default function SelectorFlow({ grade, onGradeChange }) {
|
||||
const handleClick = (g) => onGradeChange(g);
|
||||
|
||||
return (
|
||||
<div className="selector-flow">
|
||||
<div className="selector-flow__content">
|
||||
<h3 className="selector-flow__title">Select your standard</h3>
|
||||
<p className="selector-flow__subtitle">Choose the grade you are studying in</p>
|
||||
<div className="selector-flow__options">
|
||||
{grades.map((g) => (
|
||||
<button
|
||||
key={g}
|
||||
className={`selector-flow__option${grade === g ? " selector-flow__option--selected" : ""}`}
|
||||
onClick={() => handleClick(g)}
|
||||
>
|
||||
<span className="selector-flow__icon">school</span>
|
||||
<span className="selector-flow__label">{g}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -62,12 +62,42 @@
|
||||
font-size: var(--font-size-label-md);
|
||||
font-weight: var(--font-weight-medium);
|
||||
cursor: pointer;
|
||||
transition: opacity var(--transition-fast);
|
||||
transition: all var(--transition-normal);
|
||||
margin-bottom: 24px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar__new-chat::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, transparent 50%);
|
||||
opacity: 0;
|
||||
transition: opacity var(--transition-fast);
|
||||
}
|
||||
|
||||
.sidebar__new-chat:hover {
|
||||
opacity: 0.9;
|
||||
background-color: #1a1a1a;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.sidebar__new-chat:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sidebar__new-chat:active {
|
||||
transform: translateY(0);
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.sidebar__new-chat-icon {
|
||||
transition: transform var(--transition-normal);
|
||||
}
|
||||
|
||||
.sidebar__new-chat:hover .sidebar__new-chat-icon {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
/* Section */
|
||||
@@ -78,9 +108,28 @@
|
||||
.sidebar__section--history {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.sidebar__section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.sidebar__chat-count {
|
||||
font-size: var(--font-size-label-xs);
|
||||
font-weight: var(--font-weight-bold);
|
||||
color: var(--color-on-surface-variant);
|
||||
background-color: var(--color-surface-container);
|
||||
padding: 2px 8px;
|
||||
border-radius: var(--radius-sm);
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.sidebar__section-title {
|
||||
font-size: 11px;
|
||||
font-weight: var(--font-weight-bold);
|
||||
@@ -88,7 +137,6 @@
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
padding: 0 24px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
/* Subject List */
|
||||
@@ -100,6 +148,7 @@
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
/* Subject Link — Enhanced Hover Effects */
|
||||
.sidebar__link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -115,23 +164,60 @@
|
||||
font-weight: var(--font-weight-medium);
|
||||
color: var(--color-on-surface-variant);
|
||||
text-align: left;
|
||||
transition: background-color var(--transition-fast), color var(--transition-fast);
|
||||
transition: all var(--transition-fast);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Slide indicator — hidden by default, slides in on hover */
|
||||
.sidebar__link-indicator {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%) scaleY(0);
|
||||
width: 3px;
|
||||
height: 60%;
|
||||
background-color: var(--color-primary);
|
||||
border-radius: 0 2px 2px 0;
|
||||
transition: transform var(--transition-fast);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.sidebar__link:hover {
|
||||
background-color: var(--color-surface-low);
|
||||
color: var(--color-on-surface);
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.sidebar__link:hover .sidebar__link-indicator {
|
||||
transform: translateY(-50%) scaleY(1);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sidebar__link:hover .sidebar__icon {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.sidebar__link--active {
|
||||
background-color: var(--color-surface-low);
|
||||
color: var(--color-primary);
|
||||
font-weight: var(--font-weight-bold);
|
||||
border-left: 2px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.sidebar__link--active .sidebar__link-indicator {
|
||||
transform: translateY(-50%) scaleY(1);
|
||||
opacity: 1;
|
||||
background-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.sidebar__link--active .sidebar__icon {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.sidebar__icon {
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
transition: color var(--transition-fast);
|
||||
}
|
||||
|
||||
/* Chat List */
|
||||
@@ -143,10 +229,24 @@
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
/* Chat Item — wrapper for link + delete */
|
||||
.sidebar__chat-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: var(--radius-md);
|
||||
transition: all var(--transition-fast);
|
||||
}
|
||||
|
||||
.sidebar__chat-item:hover {
|
||||
background-color: var(--color-surface-low);
|
||||
}
|
||||
|
||||
/* Chat Link */
|
||||
.sidebar__chat-link {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
width: calc(100% - 32px);
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
@@ -154,7 +254,8 @@
|
||||
cursor: pointer;
|
||||
font-family: var(--font-body);
|
||||
text-align: left;
|
||||
transition: background-color var(--transition-fast);
|
||||
transition: all var(--transition-fast);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.sidebar__chat-link:hover {
|
||||
@@ -163,6 +264,7 @@
|
||||
|
||||
.sidebar__chat-link--active {
|
||||
background-color: var(--color-surface-low);
|
||||
border-left: 2px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.sidebar__chat-preview {
|
||||
@@ -172,6 +274,7 @@
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sidebar__chat-time {
|
||||
@@ -181,6 +284,87 @@
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* Delete Button — hidden until hover */
|
||||
.sidebar__chat-delete {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
background: transparent;
|
||||
cursor: pointer;
|
||||
color: var(--color-on-surface-variant);
|
||||
opacity: 0;
|
||||
transform: translateX(4px);
|
||||
transition: all var(--transition-fast);
|
||||
flex-shrink: 0;
|
||||
margin-left: auto;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.sidebar__chat-item:hover .sidebar__chat-delete {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.sidebar__chat-delete:hover {
|
||||
background-color: #fee2e2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.sidebar__chat-delete .material-symbols-outlined {
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.sidebar__chat-delete--visible {
|
||||
opacity: 0.6;
|
||||
transform: translateX(0);
|
||||
}
|
||||
|
||||
.sidebar__chat-delete--visible:hover {
|
||||
opacity: 1;
|
||||
background-color: #fee2e2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
/* Loading State */
|
||||
.sidebar__loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 24px 16px;
|
||||
color: var(--color-on-surface-variant);
|
||||
font-size: var(--font-size-label-md);
|
||||
}
|
||||
|
||||
.sidebar__loading-spinner {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid var(--color-outline-variant);
|
||||
border-top-color: var(--color-primary);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
/* Empty State */
|
||||
.sidebar__empty {
|
||||
padding: 24px 16px;
|
||||
color: var(--color-on-surface-variant);
|
||||
font-size: var(--font-size-label-md);
|
||||
text-align: center;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
.sidebar__footer {
|
||||
display: flex;
|
||||
@@ -234,11 +418,12 @@
|
||||
gap: 8px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: border-color var(--transition-fast);
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast);
|
||||
}
|
||||
|
||||
.sidebar__upgrade:hover {
|
||||
border-color: var(--color-outline);
|
||||
box-shadow: var(--shadow-card-hover);
|
||||
}
|
||||
|
||||
.sidebar__upgrade-header {
|
||||
@@ -278,4 +463,5 @@
|
||||
|
||||
.sidebar__upgrade:hover .sidebar__upgrade-arrow {
|
||||
color: var(--color-primary);
|
||||
transform: translateY(-50%) translateX(2px);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,16 @@
|
||||
|
||||
import "./Sidebar.css";
|
||||
|
||||
const Sidebar = ({ activeSubject, onSubjectChange, activeChat, onChatSelect }) => {
|
||||
const Sidebar = ({
|
||||
activeSubject,
|
||||
onSubjectChange,
|
||||
activeChat,
|
||||
onChatSelect,
|
||||
onNewChat,
|
||||
sessions,
|
||||
onDeleteChat,
|
||||
isLoadingSessions,
|
||||
}) => {
|
||||
const subjects = [
|
||||
{ id: "math", name: "Mathematics", icon: "functions" },
|
||||
{ id: "science", name: "Science", icon: "science" },
|
||||
@@ -14,12 +23,20 @@ const Sidebar = ({ activeSubject, onSubjectChange, activeChat, onChatSelect }) =
|
||||
{ id: "geography", name: "Geography", icon: "public" },
|
||||
];
|
||||
|
||||
const chats = [
|
||||
{ id: 1, preview: "Explain photosynthesis", time: "Today, 10:42 AM" },
|
||||
{ id: 2, preview: "Help with integrals", time: "Yesterday, 3:15 PM" },
|
||||
{ id: 3, preview: "What is cell respiration?", time: "Yesterday, 11:08 AM" },
|
||||
{ id: 4, preview: "Newton's laws of motion", time: "May 18, 6:20 PM" },
|
||||
];
|
||||
const formatTime = (isoString) => {
|
||||
const date = new Date(isoString);
|
||||
const now = new Date();
|
||||
const diffMs = now - date;
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHrs = Math.floor(diffMs / 3600000);
|
||||
const diffDays = Math.floor(diffMs / 86400000);
|
||||
|
||||
if (diffMins < 1) return "Just now";
|
||||
if (diffMins < 60) return `${diffMins}m ago`;
|
||||
if (diffHrs < 24) return `${diffHrs}h ago`;
|
||||
if (diffDays < 7) return `${diffDays}d ago`;
|
||||
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
||||
};
|
||||
|
||||
return (
|
||||
<aside className="sidebar">
|
||||
@@ -32,8 +49,8 @@ const Sidebar = ({ activeSubject, onSubjectChange, activeChat, onChatSelect }) =
|
||||
</div>
|
||||
|
||||
{/* New Chat Button */}
|
||||
<button className="sidebar__new-chat">
|
||||
<span className="material-symbols-outlined">add</span>
|
||||
<button className="sidebar__new-chat" onClick={onNewChat} aria-label="Start new chat">
|
||||
<span className="material-symbols-outlined sidebar__new-chat-icon">add</span>
|
||||
<span>New Chat</span>
|
||||
</button>
|
||||
|
||||
@@ -47,6 +64,7 @@ const Sidebar = ({ activeSubject, onSubjectChange, activeChat, onChatSelect }) =
|
||||
className={`sidebar__link ${activeSubject === subject.id ? "sidebar__link--active" : ""}`}
|
||||
onClick={() => onSubjectChange(subject.id)}
|
||||
>
|
||||
<span className={`sidebar__link-indicator`} />
|
||||
<span className={`material-symbols-outlined sidebar__icon`}>{subject.icon}</span>
|
||||
<span className="sidebar__label">{subject.name}</span>
|
||||
</button>
|
||||
@@ -57,20 +75,48 @@ const Sidebar = ({ activeSubject, onSubjectChange, activeChat, onChatSelect }) =
|
||||
|
||||
{/* Chat History Section */}
|
||||
<div className="sidebar__section sidebar__section--history">
|
||||
<div className="sidebar__section-header">
|
||||
<h2 className="sidebar__section-title">Chat History</h2>
|
||||
<span className="sidebar__chat-count">{sessions.length}</span>
|
||||
</div>
|
||||
{isLoadingSessions ? (
|
||||
<div className="sidebar__loading">
|
||||
<span className="sidebar__loading-spinner" />
|
||||
<span>Loading...</span>
|
||||
</div>
|
||||
) : sessions.length === 0 ? (
|
||||
<p className="sidebar__empty">No chats yet</p>
|
||||
) : (
|
||||
<ul className="sidebar__chat-list">
|
||||
{chats.map((chat) => (
|
||||
<li key={chat.id}>
|
||||
{sessions.map((session) => {
|
||||
const preview = session.preview || session.subject || "New Chat";
|
||||
const isActive = activeChat === session.id;
|
||||
|
||||
return (
|
||||
<li key={session.id} className="sidebar__chat-item">
|
||||
<button
|
||||
className={`sidebar__chat-link ${activeChat === chat.id ? "sidebar__chat-link--active" : ""}`}
|
||||
onClick={() => onChatSelect(chat.id)}
|
||||
className={`sidebar__chat-link ${isActive ? "sidebar__chat-link--active" : ""}`}
|
||||
onClick={() => onChatSelect(session.id)}
|
||||
>
|
||||
<span className="sidebar__chat-preview">{chat.preview}</span>
|
||||
<span className="sidebar__chat-time">{chat.time}</span>
|
||||
<span className="sidebar__chat-preview">{preview}</span>
|
||||
<span className="sidebar__chat-time">{formatTime(session.updatedAt)}</span>
|
||||
</button>
|
||||
<button
|
||||
className={`sidebar__chat-delete ${isActive ? "sidebar__chat-delete--visible" : ""}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onDeleteChat(session.id);
|
||||
}}
|
||||
aria-label="Delete chat"
|
||||
title="Delete chat"
|
||||
>
|
||||
<span className="material-symbols-outlined">delete</span>
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
|
||||
@@ -6,7 +6,13 @@ export default defineConfig({
|
||||
root: ".",
|
||||
publicDir: "public",
|
||||
server: {
|
||||
port: 3000,
|
||||
port: 5173,
|
||||
open: true,
|
||||
proxy: {
|
||||
"/api": {
|
||||
target: "http://localhost:3001",
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Generated
+315
@@ -15,6 +15,7 @@
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.4",
|
||||
"concurrently": "^9.2.1",
|
||||
"oxlint": "^1.75.0",
|
||||
"vite": "^8.2.0"
|
||||
}
|
||||
@@ -685,6 +686,122 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
|
||||
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
"supports-color": "^7.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/chalk?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/chalk/node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/cliui": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
"strip-ansi": "^6.0.1",
|
||||
"wrap-ansi": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/concurrently": {
|
||||
"version": "9.2.4",
|
||||
"resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.4.tgz",
|
||||
"integrity": "sha512-TZ0CEhyzvFjgtAvHTusDMgj7wNdihCh7LLLrzdUOXIhdlnL2JBBGA9eJxR24rtqgmdjh3OA3hrN1rCHj6HM8qA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chalk": "4.1.2",
|
||||
"rxjs": "7.8.2",
|
||||
"shell-quote": "1.9.0",
|
||||
"supports-color": "8.1.1",
|
||||
"tree-kill": "1.2.2",
|
||||
"yargs": "17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"conc": "dist/bin/concurrently.js",
|
||||
"concurrently": "dist/bin/concurrently.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
@@ -702,6 +819,23 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||
@@ -735,6 +869,36 @@
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/get-caller-file": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
}
|
||||
},
|
||||
"node_modules/has-flag": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.33.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
|
||||
@@ -1146,6 +1310,16 @@
|
||||
"react": "^19.2.8"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/rolldown": {
|
||||
"version": "1.2.4",
|
||||
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.4.tgz",
|
||||
@@ -1179,12 +1353,35 @@
|
||||
"@rolldown/binding-win32-x64-msvc": "1.2.4"
|
||||
}
|
||||
},
|
||||
"node_modules/rxjs": {
|
||||
"version": "7.8.2",
|
||||
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
|
||||
"integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.27.0",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/shell-quote": {
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz",
|
||||
"integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -1195,6 +1392,50 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
|
||||
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-flag": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/supports-color?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.17",
|
||||
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||
@@ -1212,6 +1453,23 @@
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tree-kill": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
|
||||
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"tree-kill": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "8.2.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz",
|
||||
@@ -1289,6 +1547,63 @@
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs": {
|
||||
"version": "17.7.2",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
|
||||
"integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
"escalade": "^3.1.1",
|
||||
"get-caller-file": "^2.0.5",
|
||||
"require-directory": "^2.1.1",
|
||||
"string-width": "^4.2.3",
|
||||
"y18n": "^5.0.5",
|
||||
"yargs-parser": "^21.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/yargs-parser": {
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+5
-3
@@ -4,9 +4,10 @@
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"lint": "oxlint",
|
||||
"dev": "concurrently \"npm run dev:backend\" \"npm run dev:frontend\"",
|
||||
"dev:frontend": "cd frontend && vite",
|
||||
"dev:backend": "cd backend && npm run dev",
|
||||
"build": "cd frontend && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -17,6 +18,7 @@
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.4",
|
||||
"concurrently": "^9.2.1",
|
||||
"oxlint": "^1.75.0",
|
||||
"vite": "^8.2.0"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user