Before Fine Tune
This commit is contained in:
6
FineTune/.env.example
Normal file
6
FineTune/.env.example
Normal file
@@ -0,0 +1,6 @@
|
||||
FINE_TUNE_PORT=4310
|
||||
FINE_TUNE_AI_ENDPOINT=http://moku-a100:8000/v1/chat/completions
|
||||
FINE_TUNE_AI_API_KEY=WnJN_dq5BC3KCb0dJqHUtAWWqjBHhMJxLhDQIwCug5w
|
||||
FINE_TUNE_AI_MODEL=qwen3.6-27b
|
||||
FINE_TUNE_BACKEND_URL=https://boost.ai.moku.build
|
||||
FINE_TUNE_BACKEND_TOKEN=replace-with-teacher-session-jwt
|
||||
81
FineTune/README.md
Normal file
81
FineTune/README.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# FineTune Helper
|
||||
|
||||
Local helper app for creating assignment-level fine-tuning records.
|
||||
|
||||
## Start
|
||||
|
||||
1. Copy `.env.example` to `.env`
|
||||
2. Fill in the hosted AI endpoint, key, and model
|
||||
3. Fill in the backend generator URL and teacher token
|
||||
4. Run from repo root:
|
||||
|
||||
```bash
|
||||
make fine-tune
|
||||
```
|
||||
|
||||
Then open:
|
||||
|
||||
```text
|
||||
http://localhost:4310
|
||||
```
|
||||
|
||||
## What it does now
|
||||
|
||||
- generates a full assignment from the real backend `POST /api/questions/generate` endpoint using:
|
||||
- `topic`
|
||||
- `difficulty`
|
||||
- `count`
|
||||
- stores the assignment in the same shape the real review flow expects:
|
||||
- assignment metadata
|
||||
- question list
|
||||
- student submission per question
|
||||
- teacher review per question
|
||||
- assignment summary
|
||||
- recommended next step
|
||||
- can ask the hosted model to draft:
|
||||
- the full student submission for all questions
|
||||
- the full teacher review package for all questions plus assignment summary
|
||||
- shows:
|
||||
- a canonical saved record preview
|
||||
- a chat-style fine-tune JSON preview
|
||||
- saves reviewed examples locally in your browser
|
||||
- lets you load, update, and delete saved examples
|
||||
- exports either:
|
||||
- `dataset.jsonl`
|
||||
- `train.jsonl` + `val.jsonl`
|
||||
|
||||
## Saved record shape
|
||||
|
||||
The helper now targets one saved row per assignment:
|
||||
|
||||
```text
|
||||
assignment-review-v1
|
||||
assignment
|
||||
studentSubmission
|
||||
teacherReview.questions[]
|
||||
teacherReview.assignmentSummary
|
||||
teacherReview.recommendedNextStep
|
||||
```
|
||||
|
||||
This matches the real app's mixed-granularity review flow:
|
||||
|
||||
```text
|
||||
one assignment review call
|
||||
-> question-level labels for every question
|
||||
-> one assignment-level summary
|
||||
```
|
||||
|
||||
## Backend generator auth
|
||||
|
||||
Set:
|
||||
|
||||
- `FINE_TUNE_BACKEND_URL` to your BoostAI base URL, for example `https://boost.ai.moku.build`
|
||||
- `FINE_TUNE_BACKEND_TOKEN` to a valid teacher JWT/session token value
|
||||
|
||||
The helper forwards that token as:
|
||||
|
||||
```text
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
so it can call the protected backend generator endpoint from the separate local helper app.
|
||||
21
FineTune/docker-compose.yaml
Normal file
21
FineTune/docker-compose.yaml
Normal file
@@ -0,0 +1,21 @@
|
||||
services:
|
||||
fine-tune-helper:
|
||||
image: node:22-alpine
|
||||
working_dir: /app
|
||||
command: sh -lc "npm ci && npm run dev"
|
||||
ports:
|
||||
- "127.0.0.1:${FINE_TUNE_PORT:-4310}:4310"
|
||||
environment:
|
||||
PORT: 4310
|
||||
HOST: 0.0.0.0
|
||||
FINE_TUNE_AI_ENDPOINT: ${FINE_TUNE_AI_ENDPOINT:-}
|
||||
FINE_TUNE_AI_API_KEY: ${FINE_TUNE_AI_API_KEY:-}
|
||||
FINE_TUNE_AI_MODEL: ${FINE_TUNE_AI_MODEL:-}
|
||||
FINE_TUNE_BACKEND_URL: ${FINE_TUNE_BACKEND_URL:-}
|
||||
FINE_TUNE_BACKEND_TOKEN: ${FINE_TUNE_BACKEND_TOKEN:-}
|
||||
volumes:
|
||||
- ./:/app
|
||||
- fine_tune_node_modules:/app/node_modules
|
||||
|
||||
volumes:
|
||||
fine_tune_node_modules:
|
||||
850
FineTune/package-lock.json
generated
Normal file
850
FineTune/package-lock.json
generated
Normal file
@@ -0,0 +1,850 @@
|
||||
{
|
||||
"name": "boostai-fine-tune-helper",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "boostai-fine-tune-helper",
|
||||
"dependencies": {
|
||||
"express": "^4.21.2",
|
||||
"ws": "^8.21.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
}
|
||||
},
|
||||
"node_modules/accepts": {
|
||||
"version": "1.3.8",
|
||||
"resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
|
||||
"integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-types": "~2.1.34",
|
||||
"negotiator": "0.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz",
|
||||
"integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.5",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
|
||||
"integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "~3.1.2",
|
||||
"content-type": "~1.0.5",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "~1.2.0",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.4.24",
|
||||
"on-finished": "~2.4.1",
|
||||
"qs": "~6.15.1",
|
||||
"raw-body": "~2.5.3",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"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": "0.5.4",
|
||||
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz",
|
||||
"integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "5.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"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.0.7",
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz",
|
||||
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"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/destroy": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
|
||||
"integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.8",
|
||||
"npm": "1.2.8000 || >= 1.4.16"
|
||||
}
|
||||
},
|
||||
"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": "4.22.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
|
||||
"integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "~1.20.5",
|
||||
"content-disposition": "~0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "~0.7.1",
|
||||
"cookie-signature": "~1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "~1.3.1",
|
||||
"fresh": "~0.5.2",
|
||||
"http-errors": "~2.0.0",
|
||||
"merge-descriptors": "1.0.3",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "~2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "~0.1.12",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "~6.15.1",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "~0.19.0",
|
||||
"serve-static": "~1.16.2",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "~2.0.1",
|
||||
"type-is": "~1.6.18",
|
||||
"utils-merge": "1.0.1",
|
||||
"vary": "~1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.10.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/express"
|
||||
}
|
||||
},
|
||||
"node_modules/finalhandler": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz",
|
||||
"integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"on-finished": "~2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"statuses": "~2.0.2",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"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": "0.5.2",
|
||||
"resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz",
|
||||
"integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"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.3",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
|
||||
"integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
|
||||
"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.4.24",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
|
||||
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"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/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": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/methods": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz",
|
||||
"integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
|
||||
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/negotiator": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
|
||||
"integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"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/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": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
|
||||
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"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.2",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
|
||||
"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
"integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/raw-body": {
|
||||
"version": "2.5.3",
|
||||
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
|
||||
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bytes": "~3.1.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"iconv-lite": "~0.4.24",
|
||||
"unpipe": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"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": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz",
|
||||
"integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"destroy": "1.2.0",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"fresh": "~0.5.2",
|
||||
"http-errors": "~2.0.1",
|
||||
"mime": "1.6.0",
|
||||
"ms": "2.1.3",
|
||||
"on-finished": "~2.4.1",
|
||||
"range-parser": "~1.2.1",
|
||||
"statuses": "~2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/send/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/serve-static": {
|
||||
"version": "1.16.3",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz",
|
||||
"integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"parseurl": "~1.3.3",
|
||||
"send": "~0.19.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"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.0",
|
||||
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
|
||||
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"object-inspect": "^1.13.3",
|
||||
"side-channel-list": "^1.0.0",
|
||||
"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": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"media-typer": "0.3.0",
|
||||
"mime-types": "~2.1.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"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/utils-merge": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",
|
||||
"integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"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/ws": {
|
||||
"version": "8.21.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz",
|
||||
"integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
16
FineTune/package.json
Normal file
16
FineTune/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "boostai-fine-tune-helper",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "node server.mjs",
|
||||
"start": "node server.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.21.2",
|
||||
"ws": "^8.21.0"
|
||||
}
|
||||
}
|
||||
1844
FineTune/public/app.js
Normal file
1844
FineTune/public/app.js
Normal file
File diff suppressed because it is too large
Load Diff
250
FineTune/public/index.html
Normal file
250
FineTune/public/index.html
Normal file
@@ -0,0 +1,250 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>BoostAI FineTune Helper</title>
|
||||
<link rel="stylesheet" href="/styles.css" />
|
||||
</head>
|
||||
<body>
|
||||
<div class="page-shell">
|
||||
<header class="hero">
|
||||
<div>
|
||||
<p class="eyebrow">Local helper app</p>
|
||||
<h1>BoostAI FineTune Data Helper</h1>
|
||||
<p class="hero-copy">
|
||||
Create full assignment-review examples that match the real app shape: one assignment, many question-level labels, and one assignment-level summary.
|
||||
</p>
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<button id="save-example" type="button">Save assignment example</button>
|
||||
<button id="load-sample" type="button">Load sample</button>
|
||||
<button id="clear-form" type="button" class="button-secondary">Clear</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="status-bar">
|
||||
<div>
|
||||
<strong>Hosted AI:</strong>
|
||||
<span id="ai-config-status">Checking…</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Assignment generator:</strong>
|
||||
<span id="question-generator-status">Checking…</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Shared workspace:</strong>
|
||||
<span id="collab-status">Connecting…</span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Editors:</strong>
|
||||
<span id="presence-status">1 connected</span>
|
||||
</div>
|
||||
<div id="save-status">Autosave ready</div>
|
||||
</section>
|
||||
|
||||
<main class="layout-grid">
|
||||
<section class="editor-shell span-2">
|
||||
<aside class="card workspace-nav-card">
|
||||
<div class="workspace-toolbar">
|
||||
<div>
|
||||
<h2>Assignments</h2>
|
||||
<p class="section-copy">Search, sort, and switch between lots of in-progress assignment drafts without losing the shared editor.</p>
|
||||
</div>
|
||||
<button id="new-draft" type="button">New draft</button>
|
||||
</div>
|
||||
|
||||
<div class="workspace-controls">
|
||||
<label class="control-field workspace-search-field">
|
||||
<span>Find an assignment</span>
|
||||
<input id="draft-search" type="text" placeholder="Search by title, assignment ID, topic, or student" />
|
||||
</label>
|
||||
<label class="control-field control-field-compact">
|
||||
<span>Sort</span>
|
||||
<select id="draft-sort">
|
||||
<option value="updated">Recently updated</option>
|
||||
<option value="title">Title</option>
|
||||
<option value="topic">Topic</option>
|
||||
<option value="questions">Question count</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="workspace-meta-row">
|
||||
<div id="draft-status" class="summary-strip compact-strip">1 local draft</div>
|
||||
<div class="workspace-actions">
|
||||
<button id="duplicate-draft" type="button" class="button-secondary">Duplicate</button>
|
||||
<button id="rename-draft" type="button" class="button-secondary">Rename</button>
|
||||
<button id="delete-draft" type="button" class="button-danger">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="draft-list" class="draft-list" role="listbox" aria-label="Assignment drafts"></div>
|
||||
</aside>
|
||||
|
||||
<section class="card editor-card">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h2>Assignment builder</h2>
|
||||
<p class="section-copy">Generate a multi-question assignment from the real backend, then edit the assignment metadata or any question before saving.</p>
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<button id="generate-assignment" type="button">Generate assignment</button>
|
||||
<button id="add-question" type="button" class="button-secondary">Add blank question</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-grid three-up">
|
||||
<label>
|
||||
<span>Assignment ID</span>
|
||||
<input id="assignmentId" type="text" placeholder="assignment-fractions-01" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Student ID</span>
|
||||
<input id="studentId" type="text" placeholder="student-17" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Pass threshold</span>
|
||||
<input id="passThreshold" type="number" min="0" max="1" step="0.01" placeholder="0.70" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-grid two-up">
|
||||
<label>
|
||||
<span>Assignment title</span>
|
||||
<input id="assignmentTitle" type="text" placeholder="Fractions practice review" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Instructions</span>
|
||||
<input id="instructions" type="text" placeholder="Show working for every question." />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-grid four-up">
|
||||
<label>
|
||||
<span>Topic</span>
|
||||
<select id="topic">
|
||||
<option value="">Select…</option>
|
||||
<option value="place_value">place_value</option>
|
||||
<option value="arithmetic">arithmetic</option>
|
||||
<option value="negative_numbers">negative_numbers</option>
|
||||
<option value="bidmas">bidmas</option>
|
||||
<option value="fractions">fractions</option>
|
||||
<option value="algebra">algebra</option>
|
||||
<option value="geometry">geometry</option>
|
||||
<option value="data">data</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Difficulty</span>
|
||||
<select id="difficulty">
|
||||
<option value="">Select…</option>
|
||||
<option value="easy">easy</option>
|
||||
<option value="medium">medium</option>
|
||||
<option value="hard">hard</option>
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Question count</span>
|
||||
<input id="questionCount" type="number" min="1" max="25" step="1" placeholder="4" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Generation seed (optional)</span>
|
||||
<input id="generatorSeed" type="text" placeholder="auto-filled after generation" readonly />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="question-toolbar">
|
||||
<div class="question-toolbar-group">
|
||||
<span class="question-toolbar-label">View</span>
|
||||
<button id="filter-all" type="button" class="button-secondary is-active">All questions</button>
|
||||
<button id="filter-attention" type="button" class="button-secondary">Needs attention</button>
|
||||
<button id="filter-unlabeled" type="button" class="button-secondary">Unlabeled</button>
|
||||
</div>
|
||||
<div class="question-toolbar-group">
|
||||
<span class="question-toolbar-label">Layout</span>
|
||||
<button id="expand-all-questions" type="button" class="button-secondary">Expand all</button>
|
||||
<button id="collapse-all-questions" type="button" class="button-secondary">Collapse all</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="question-summary" class="summary-strip">No questions yet.</div>
|
||||
<div id="questions-container" class="question-stack"></div>
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h2>Student submission</h2>
|
||||
<p class="section-copy">Draft the whole student submission at once, then tweak any question manually.</p>
|
||||
</div>
|
||||
<button id="generate-students" type="button">AI draft student submission</button>
|
||||
</div>
|
||||
<div class="mini-note">
|
||||
One student voice should carry across the full assignment. Each question stores <code>answerText</code>, <code>workingSteps</code>, and <code>solveMode</code>.
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h2>Teacher review package</h2>
|
||||
<p class="section-copy">Generate per-question review labels plus one assignment summary, matching the real production shape.</p>
|
||||
</div>
|
||||
<button id="generate-teacher" type="button">AI draft full teacher review</button>
|
||||
</div>
|
||||
|
||||
<label>
|
||||
<span>Assignment summary</span>
|
||||
<textarea id="assignmentSummary" rows="5" placeholder="Short whole-assignment summary"></textarea>
|
||||
</label>
|
||||
<label>
|
||||
<span>Recommended next step</span>
|
||||
<textarea id="recommendedNextStep" rows="4" placeholder="What the teacher should do next"></textarea>
|
||||
</label>
|
||||
</section>
|
||||
|
||||
<section class="card preview-card">
|
||||
<div class="section-heading">
|
||||
<h2>Labeled record preview</h2>
|
||||
<button id="copy-record" type="button" class="button-secondary">Copy JSON</button>
|
||||
</div>
|
||||
<pre id="recordPreview"></pre>
|
||||
</section>
|
||||
|
||||
<section class="card preview-card">
|
||||
<div class="section-heading">
|
||||
<h2>Fine-tune example preview</h2>
|
||||
<button id="copy-training" type="button" class="button-secondary">Copy JSON</button>
|
||||
</div>
|
||||
<pre id="trainingPreview"></pre>
|
||||
</section>
|
||||
|
||||
<section class="card dataset-card span-2">
|
||||
<div class="section-heading">
|
||||
<div>
|
||||
<h2>Saved dataset</h2>
|
||||
<p id="datasetStatus" class="section-copy">No saved examples yet.</p>
|
||||
</div>
|
||||
<div class="hero-actions dataset-actions">
|
||||
<button id="export-dataset" type="button" class="button-secondary">Export dataset.jsonl</button>
|
||||
<button id="export-split" type="button" class="button-secondary">Export train/val split</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="datasetEmpty" class="empty-state">
|
||||
Save full assignment-review examples here. Each saved item becomes one training row.
|
||||
</div>
|
||||
<div id="datasetList" class="dataset-list"></div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<section class="card footer-note">
|
||||
<strong>Shape target:</strong> one assignment-level review call, question-level labels for every question, and one assignment summary. Treat AI drafts as prefill only.
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div id="toast" class="toast hidden"></div>
|
||||
<script type="module" src="/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
679
FineTune/public/styles.css
Normal file
679
FineTune/public/styles.css
Normal file
@@ -0,0 +1,679 @@
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--bg: #f6f4fb;
|
||||
--panel: #ffffff;
|
||||
--panel-alt: #f9f7fd;
|
||||
--subpanel: #f3effc;
|
||||
--text: #211a2f;
|
||||
--muted: #6c6383;
|
||||
--accent: #7443da;
|
||||
--accent-soft: #ede6fb;
|
||||
--border: #ddd5ef;
|
||||
--success: #0d8f4f;
|
||||
--danger: #b63a59;
|
||||
--shadow: 0 18px 40px rgba(43, 29, 76, 0.08);
|
||||
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background: linear-gradient(180deg, #faf8ff 0%, var(--bg) 100%);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
border-radius: 12px;
|
||||
padding: 0.8rem 1rem;
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
filter: brightness(1.03);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: progress;
|
||||
}
|
||||
|
||||
.button-secondary {
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.button-danger {
|
||||
background: #fbe7ed;
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.button-compact {
|
||||
padding: 0.55rem 0.8rem;
|
||||
border-radius: 10px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.page-shell {
|
||||
max-width: 1600px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
padding-bottom: 4rem;
|
||||
}
|
||||
|
||||
.hero,
|
||||
.status-bar,
|
||||
.card {
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 24px;
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 1.5rem 1.75rem;
|
||||
align-items: end;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
margin: 0 0 0.5rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 800;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.hero h1 {
|
||||
margin: 0;
|
||||
font-size: clamp(2rem, 4vw, 3rem);
|
||||
}
|
||||
|
||||
.hero-copy {
|
||||
margin: 0.75rem 0 0;
|
||||
max-width: 70ch;
|
||||
color: var(--muted);
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.hero-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: 0.95rem 1.25rem;
|
||||
margin-bottom: 1rem;
|
||||
color: var(--muted);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.layout-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.editor-shell {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(300px, 360px) minmax(0, 1fr);
|
||||
gap: 1rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.span-2,
|
||||
.dataset-card {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 1.25rem;
|
||||
}
|
||||
|
||||
.card h2,
|
||||
.card h3,
|
||||
.card h4 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.section-copy {
|
||||
margin: 0.35rem 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.two-up {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.three-up {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.four-up {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
label {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
label span {
|
||||
font-weight: 700;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea,
|
||||
select {
|
||||
width: 100%;
|
||||
border-radius: 14px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--panel-alt);
|
||||
padding: 0.9rem 1rem;
|
||||
color: var(--text);
|
||||
line-height: 1.5;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
input:focus,
|
||||
textarea:focus,
|
||||
select:focus {
|
||||
outline: 2px solid rgba(116, 67, 218, 0.18);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.summary-strip,
|
||||
.mini-note {
|
||||
padding: 0.9rem 1rem;
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--border);
|
||||
background: #fcfbff;
|
||||
color: var(--muted);
|
||||
margin-bottom: 1rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.question-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.9rem 1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 18px;
|
||||
background: #fcfbff;
|
||||
}
|
||||
|
||||
.question-toolbar-group {
|
||||
display: flex;
|
||||
gap: 0.6rem;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.question-toolbar-label {
|
||||
font-size: 0.86rem;
|
||||
font-weight: 800;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.question-toolbar .button-secondary.is-active {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.question-stack {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.question-card {
|
||||
padding: 1rem;
|
||||
border-radius: 20px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--panel-alt);
|
||||
display: grid;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.question-card.is-collapsed {
|
||||
gap: 0.4rem;
|
||||
}
|
||||
|
||||
.question-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.question-card-header p {
|
||||
margin: 0.35rem 0 0;
|
||||
color: var(--muted);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.question-card-heading {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.question-heading-topline {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.question-card-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.question-status-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.question-card-body {
|
||||
display: grid;
|
||||
gap: 0.8rem;
|
||||
}
|
||||
|
||||
.question-card-body[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.subpanel {
|
||||
padding: 1rem;
|
||||
border-radius: 18px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--subpanel);
|
||||
}
|
||||
|
||||
.subpanel h4 {
|
||||
margin-bottom: 0.8rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.dataset-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 1rem 1.1rem;
|
||||
border: 1px dashed var(--border);
|
||||
border-radius: 16px;
|
||||
background: #fcfbff;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.dataset-list {
|
||||
display: grid;
|
||||
gap: 0.9rem;
|
||||
}
|
||||
|
||||
.dataset-item {
|
||||
display: grid;
|
||||
gap: 0.8rem;
|
||||
padding: 1rem 1.1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 18px;
|
||||
background: var(--panel-alt);
|
||||
}
|
||||
|
||||
.dataset-item.active {
|
||||
border-color: var(--accent);
|
||||
box-shadow: inset 0 0 0 1px rgba(116, 67, 218, 0.15);
|
||||
}
|
||||
|
||||
.dataset-item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.dataset-item-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.dataset-item-meta {
|
||||
margin: 0.25rem 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.92rem;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.dataset-item-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.dataset-pill {
|
||||
padding: 0.35rem 0.6rem;
|
||||
border-radius: 999px;
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.pill-success {
|
||||
background: #e3f6ec;
|
||||
color: var(--success);
|
||||
}
|
||||
|
||||
.pill-warning {
|
||||
background: #fff1d9;
|
||||
color: #a46a00;
|
||||
}
|
||||
|
||||
.pill-muted {
|
||||
background: #efedf5;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.dataset-item-actions {
|
||||
display: flex;
|
||||
gap: 0.6rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.workspace-nav-card {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
position: sticky;
|
||||
top: 1rem;
|
||||
max-height: calc(100vh - 2rem);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.workspace-toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.workspace-controls {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 160px;
|
||||
gap: 0.85rem;
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
.control-field {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.control-field-compact span {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.workspace-search-field input,
|
||||
.control-field select {
|
||||
padding-block: 0.8rem;
|
||||
}
|
||||
|
||||
.workspace-meta-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.compact-strip {
|
||||
margin-bottom: 0;
|
||||
padding: 0.75rem 0.9rem;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.workspace-actions {
|
||||
display: flex;
|
||||
gap: 0.55rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.draft-list {
|
||||
display: grid;
|
||||
gap: 0.65rem;
|
||||
overflow: auto;
|
||||
padding-right: 0.2rem;
|
||||
}
|
||||
|
||||
.draft-item {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 0.85rem;
|
||||
padding: 0.9rem 0.95rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 16px;
|
||||
background: var(--panel-alt);
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.draft-item.active {
|
||||
border-color: var(--accent);
|
||||
box-shadow: inset 0 0 0 1px rgba(116, 67, 218, 0.15);
|
||||
background: #f8f4ff;
|
||||
}
|
||||
|
||||
.draft-item-main {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
min-width: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.draft-item-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.draft-item-title {
|
||||
font-size: 0.98rem;
|
||||
font-weight: 800;
|
||||
margin: 0;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.draft-item-meta {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.draft-item-updated {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.82rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.draft-item-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.draft-item-actions button {
|
||||
min-width: 82px;
|
||||
}
|
||||
|
||||
.draft-empty-state {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.editor-card {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.dataset-item-actions button {
|
||||
padding: 0.65rem 0.9rem;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
padding: 1rem;
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--border);
|
||||
background: #171222;
|
||||
color: #f2ecff;
|
||||
overflow: auto;
|
||||
max-height: 520px;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||
}
|
||||
|
||||
.footer-note {
|
||||
margin-top: 1rem;
|
||||
color: var(--muted);
|
||||
background: #fcfbff;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.toast {
|
||||
position: fixed;
|
||||
right: 1.25rem;
|
||||
bottom: 1.25rem;
|
||||
padding: 0.9rem 1.1rem;
|
||||
border-radius: 12px;
|
||||
background: var(--text);
|
||||
color: white;
|
||||
box-shadow: var(--shadow);
|
||||
max-width: 360px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.toast.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.toast.error {
|
||||
background: var(--danger);
|
||||
}
|
||||
|
||||
.toast.success {
|
||||
background: var(--success);
|
||||
}
|
||||
|
||||
@media (max-width: 1200px) {
|
||||
.layout-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.editor-shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.span-2,
|
||||
.dataset-card {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.workspace-nav-card {
|
||||
position: static;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.four-up {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.page-shell {
|
||||
padding: 1rem;
|
||||
padding-bottom: 3rem;
|
||||
}
|
||||
|
||||
.hero,
|
||||
.status-bar,
|
||||
.section-heading,
|
||||
.dataset-item-header,
|
||||
.workspace-meta-row,
|
||||
.workspace-toolbar,
|
||||
.draft-item-row,
|
||||
.question-card-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.workspace-controls,
|
||||
.draft-item {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.draft-item-actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.question-toolbar,
|
||||
.question-toolbar-group,
|
||||
.question-heading-topline {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.two-up,
|
||||
.three-up,
|
||||
.four-up {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
801
FineTune/server.mjs
Normal file
801
FineTune/server.mjs
Normal file
@@ -0,0 +1,801 @@
|
||||
import express from "express";
|
||||
import { createServer } from "node:http";
|
||||
import crypto from "node:crypto";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, URL } from "node:url";
|
||||
import { WebSocketServer } from "ws";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
const app = express();
|
||||
const host = process.env.HOST || "0.0.0.0";
|
||||
const port = Number(process.env.PORT || 4310);
|
||||
const server = createServer(app);
|
||||
const websocketServer = new WebSocketServer({ noServer: true });
|
||||
const sharedWorkspaces = new Map();
|
||||
|
||||
app.use(express.json({ limit: "2mb" }));
|
||||
app.use(express.static(path.join(__dirname, "public")));
|
||||
|
||||
app.get("/health", (_req, res) => {
|
||||
res.json({ ok: true });
|
||||
});
|
||||
|
||||
app.get("/api/config", (_req, res) => {
|
||||
res.json({
|
||||
hasAiConfig: hasAiConfig(),
|
||||
endpoint: process.env.FINE_TUNE_AI_ENDPOINT || "",
|
||||
model: process.env.FINE_TUNE_AI_MODEL || "",
|
||||
hasQuestionGeneratorConfig: hasQuestionGeneratorConfig(),
|
||||
backendUrl: process.env.FINE_TUNE_BACKEND_URL || "",
|
||||
});
|
||||
});
|
||||
|
||||
app.post("/api/questions/generate", async (req, res) => {
|
||||
try {
|
||||
assertQuestionGeneratorConfig();
|
||||
const input = sanitizeGeneratorInput(req.body);
|
||||
assertGeneratorInput(input);
|
||||
|
||||
const result = await callQuestionGenerator(input);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
handleError(res, error);
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/assignment/generate", async (req, res) => {
|
||||
try {
|
||||
assertQuestionGeneratorConfig();
|
||||
const input = sanitizeGeneratorInput(req.body);
|
||||
assertGeneratorInput(input);
|
||||
|
||||
const result = await callQuestionGenerator(input);
|
||||
res.json({
|
||||
seed: result.seed ?? null,
|
||||
count: result.count ?? input.count,
|
||||
questions: Array.isArray(result.data) ? result.data.map(mapGeneratedQuestion) : [],
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(res, error);
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/assignment/student-draft", async (req, res) => {
|
||||
try {
|
||||
assertAiConfig();
|
||||
const input = sanitizeAssignmentInput(req.body);
|
||||
assertAssignmentForStudentDraft(input);
|
||||
|
||||
const result = await callAiJson({
|
||||
schemaName: "fine_tune_assignment_student_draft",
|
||||
schema: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
questions: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
questionId: { type: "integer" },
|
||||
answerText: { type: "string" },
|
||||
workingSteps: { type: "string" },
|
||||
solveMode: { type: "string" },
|
||||
},
|
||||
required: ["questionId", "answerText", "workingSteps", "solveMode"],
|
||||
},
|
||||
},
|
||||
},
|
||||
required: ["questions"],
|
||||
},
|
||||
systemPrompt:
|
||||
"You are helping create high-quality fine-tuning data for assignment review. Generate realistic student submissions for every question in the assignment. The work should sound like one student completed the whole assignment. Some answers may be correct, partially correct, or incorrect, but they should stay plausible and classroom-realistic. Return only the requested JSON.",
|
||||
userPrompt: buildStudentDraftPrompt(input),
|
||||
});
|
||||
|
||||
res.json({
|
||||
questions: normalizeStudentDraftQuestions(input.questions, result.questions),
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(res, error);
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/api/assignment/teacher-draft", async (req, res) => {
|
||||
try {
|
||||
assertAiConfig();
|
||||
const input = sanitizeAssignmentInput(req.body);
|
||||
assertAssignmentForTeacherDraft(input);
|
||||
|
||||
const result = await callAiJson({
|
||||
schemaName: "fine_tune_assignment_teacher_draft",
|
||||
schema: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
questions: {
|
||||
type: "array",
|
||||
items: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
questionId: { type: "integer" },
|
||||
aiFeedback: { type: "string" },
|
||||
understandingScore: { type: "number" },
|
||||
confidence: { type: "number" },
|
||||
needsAttention: { type: "boolean" },
|
||||
issueReason: { type: "string" },
|
||||
},
|
||||
required: [
|
||||
"questionId",
|
||||
"aiFeedback",
|
||||
"understandingScore",
|
||||
"confidence",
|
||||
"needsAttention",
|
||||
"issueReason",
|
||||
],
|
||||
},
|
||||
},
|
||||
assignmentSummary: { type: "string" },
|
||||
recommendedNextStep: { type: "string" },
|
||||
},
|
||||
required: ["questions", "assignmentSummary", "recommendedNextStep"],
|
||||
},
|
||||
systemPrompt:
|
||||
"You are helping create fine-tuning labels for a teacher review system. Review the full assignment in one pass. Return one question-level review for every question using the exact backend-aligned fields questionId, aiFeedback, understandingScore, confidence, needsAttention, issueReason, plus assignmentSummary and recommendedNextStep for the whole assignment. Be strict but fair. Focus on conceptual understanding, not just final correctness. Return only the requested JSON.",
|
||||
userPrompt: buildTeacherDraftPrompt(input),
|
||||
});
|
||||
|
||||
res.json({
|
||||
questions: normalizeTeacherDraftQuestions(input.questions, result.questions),
|
||||
assignmentSummary: stringOrEmpty(result.assignmentSummary),
|
||||
recommendedNextStep: stringOrEmpty(result.recommendedNextStep),
|
||||
});
|
||||
} catch (error) {
|
||||
handleError(res, error);
|
||||
}
|
||||
});
|
||||
|
||||
app.get("*", (_req, res) => {
|
||||
res.sendFile(path.join(__dirname, "public", "index.html"));
|
||||
});
|
||||
|
||||
server.on("upgrade", (req, socket, head) => {
|
||||
const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
|
||||
if (url.pathname !== "/ws") {
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
websocketServer.handleUpgrade(req, socket, head, (ws) => {
|
||||
websocketServer.emit("connection", ws, req, url);
|
||||
});
|
||||
});
|
||||
|
||||
websocketServer.on("connection", (ws, _req, url) => {
|
||||
const workspaceId = sanitizeWorkspaceId(url.searchParams.get("workspace"));
|
||||
const clientId = crypto.randomUUID();
|
||||
const workspace = getSharedWorkspace(workspaceId);
|
||||
|
||||
workspace.clients.set(clientId, ws);
|
||||
sendSocketMessage(ws, {
|
||||
type: "workspace:init",
|
||||
workspaceId,
|
||||
clientId,
|
||||
version: workspace.version,
|
||||
state: cloneJson(workspace.state),
|
||||
presenceCount: workspace.clients.size,
|
||||
updatedAt: workspace.updatedAt,
|
||||
});
|
||||
broadcastWorkspacePresence(workspaceId);
|
||||
|
||||
ws.on("message", (raw) => {
|
||||
let message;
|
||||
try {
|
||||
message = JSON.parse(String(raw));
|
||||
} catch {
|
||||
sendSocketMessage(ws, { type: "workspace:error", message: "Invalid collaboration payload." });
|
||||
return;
|
||||
}
|
||||
|
||||
if (message?.type !== "workspace:update") {
|
||||
sendSocketMessage(ws, { type: "workspace:error", message: "Unsupported collaboration message." });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isPlainObject(message.state)) {
|
||||
sendSocketMessage(ws, { type: "workspace:error", message: "Workspace state must be an object." });
|
||||
return;
|
||||
}
|
||||
|
||||
workspace.version += 1;
|
||||
workspace.state = cloneJson(message.state);
|
||||
workspace.updatedAt = new Date().toISOString();
|
||||
broadcastWorkspaceSnapshot(workspaceId, clientId);
|
||||
});
|
||||
|
||||
ws.on("close", () => {
|
||||
workspace.clients.delete(clientId);
|
||||
broadcastWorkspacePresence(workspaceId);
|
||||
});
|
||||
|
||||
ws.on("error", () => {});
|
||||
});
|
||||
|
||||
server.listen(port, host, () => {
|
||||
console.log(`FineTune helper listening on http://${host}:${port}`);
|
||||
});
|
||||
|
||||
function getSharedWorkspace(workspaceId) {
|
||||
if (!sharedWorkspaces.has(workspaceId)) {
|
||||
sharedWorkspaces.set(workspaceId, {
|
||||
version: 0,
|
||||
updatedAt: null,
|
||||
state: null,
|
||||
clients: new Map(),
|
||||
});
|
||||
}
|
||||
|
||||
return sharedWorkspaces.get(workspaceId);
|
||||
}
|
||||
|
||||
function broadcastWorkspaceSnapshot(workspaceId, actorClientId) {
|
||||
const workspace = getSharedWorkspace(workspaceId);
|
||||
broadcastWorkspaceMessage(workspaceId, {
|
||||
type: "workspace:snapshot",
|
||||
workspaceId,
|
||||
version: workspace.version,
|
||||
state: cloneJson(workspace.state),
|
||||
actorClientId,
|
||||
presenceCount: workspace.clients.size,
|
||||
updatedAt: workspace.updatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
function broadcastWorkspacePresence(workspaceId) {
|
||||
const workspace = getSharedWorkspace(workspaceId);
|
||||
broadcastWorkspaceMessage(workspaceId, {
|
||||
type: "workspace:presence",
|
||||
workspaceId,
|
||||
presenceCount: workspace.clients.size,
|
||||
version: workspace.version,
|
||||
});
|
||||
}
|
||||
|
||||
function broadcastWorkspaceMessage(workspaceId, payload) {
|
||||
const workspace = getSharedWorkspace(workspaceId);
|
||||
for (const ws of workspace.clients.values()) {
|
||||
sendSocketMessage(ws, payload);
|
||||
}
|
||||
}
|
||||
|
||||
function sendSocketMessage(ws, payload) {
|
||||
if (ws.readyState !== 1) return;
|
||||
ws.send(JSON.stringify(payload));
|
||||
}
|
||||
|
||||
function sanitizeWorkspaceId(value) {
|
||||
const raw = String(value || "shared").trim();
|
||||
if (!raw) return "shared";
|
||||
return /^[a-zA-Z0-9._-]{1,64}$/.test(raw) ? raw : "shared";
|
||||
}
|
||||
|
||||
function isPlainObject(value) {
|
||||
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function cloneJson(value) {
|
||||
return value == null ? null : JSON.parse(JSON.stringify(value));
|
||||
}
|
||||
|
||||
function hasAiConfig() {
|
||||
return Boolean(process.env.FINE_TUNE_AI_ENDPOINT && process.env.FINE_TUNE_AI_API_KEY && process.env.FINE_TUNE_AI_MODEL);
|
||||
}
|
||||
|
||||
function hasQuestionGeneratorConfig() {
|
||||
return Boolean(process.env.FINE_TUNE_BACKEND_URL && process.env.FINE_TUNE_BACKEND_TOKEN);
|
||||
}
|
||||
|
||||
function assertAiConfig() {
|
||||
if (!hasAiConfig()) {
|
||||
const error = new Error("AI config is missing. Set FINE_TUNE_AI_ENDPOINT, FINE_TUNE_AI_API_KEY, and FINE_TUNE_AI_MODEL.");
|
||||
error.status = 503;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function assertQuestionGeneratorConfig() {
|
||||
if (!hasQuestionGeneratorConfig()) {
|
||||
const error = new Error(
|
||||
"Question generator config is missing. Set FINE_TUNE_BACKEND_URL and FINE_TUNE_BACKEND_TOKEN.",
|
||||
);
|
||||
error.status = 503;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function sanitizeGeneratorInput(payload = {}) {
|
||||
return {
|
||||
topic: stringOrEmpty(payload.topic),
|
||||
difficulty: stringOrEmpty(payload.difficulty),
|
||||
count: integerOrDefault(payload.count, 1),
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizeAssignmentInput(payload = {}) {
|
||||
const rawQuestions = Array.isArray(payload.questions) ? payload.questions : [];
|
||||
|
||||
return {
|
||||
assignmentId: stringOrEmpty(payload.assignmentId),
|
||||
studentId: stringOrEmpty(payload.studentId),
|
||||
assignmentTitle: stringOrEmpty(payload.assignmentTitle),
|
||||
instructions: stringOrEmpty(payload.instructions),
|
||||
passThreshold: numberOrNull(payload.passThreshold),
|
||||
topic: stringOrEmpty(payload.topic),
|
||||
difficulty: stringOrEmpty(payload.difficulty),
|
||||
questions: rawQuestions.map((question, index) => sanitizeQuestionInput(question, index)),
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizeQuestionInput(payload = {}, index = 0) {
|
||||
return {
|
||||
questionId: integerOrNull(payload.questionId),
|
||||
position: integerOrDefault(payload.position, index + 1),
|
||||
title: stringOrEmpty(payload.title),
|
||||
prompt: stringOrEmpty(payload.prompt),
|
||||
subject: stringOrEmpty(payload.subject),
|
||||
source: stringOrEmpty(payload.source),
|
||||
difficulty: stringOrEmpty(payload.difficulty),
|
||||
correctAnswer: stringOrEmpty(payload.correctAnswer),
|
||||
workedSolution: stringOrEmpty(payload.workedSolution),
|
||||
tags: stringArray(payload.tags),
|
||||
studentAnswer: stringOrEmpty(payload.studentAnswer),
|
||||
workingSteps: stringOrEmpty(payload.workingSteps),
|
||||
solveMode: stringOrEmpty(payload.solveMode) || "show_work",
|
||||
aiFeedback: stringOrEmpty(payload.aiFeedback),
|
||||
understandingScore: numberOrNull(payload.understandingScore),
|
||||
confidence: numberOrNull(payload.confidence),
|
||||
needsAttention: booleanOrNull(payload.needsAttention),
|
||||
issueReason: stringOrEmpty(payload.issueReason),
|
||||
};
|
||||
}
|
||||
|
||||
function assertGeneratorInput(input) {
|
||||
if (!input.topic) {
|
||||
const error = new Error("Topic is required.");
|
||||
error.status = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!input.difficulty) {
|
||||
const error = new Error("Difficulty is required.");
|
||||
error.status = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!Number.isInteger(input.count) || input.count < 1 || input.count > 25) {
|
||||
const error = new Error("Question count must be between 1 and 25.");
|
||||
error.status = 400;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function assertAssignmentForStudentDraft(input) {
|
||||
assertAssignmentBase(input);
|
||||
|
||||
for (const question of input.questions) {
|
||||
if (!question.correctAnswer) {
|
||||
const error = new Error(`Question ${question.position} is missing a correct answer.`);
|
||||
error.status = 400;
|
||||
throw error;
|
||||
}
|
||||
if (!question.workedSolution) {
|
||||
const error = new Error(`Question ${question.position} is missing a worked solution.`);
|
||||
error.status = 400;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertAssignmentForTeacherDraft(input) {
|
||||
assertAssignmentBase(input);
|
||||
|
||||
for (const question of input.questions) {
|
||||
if (!question.studentAnswer && !question.workingSteps) {
|
||||
const error = new Error(`Question ${question.position} needs student work before teacher review can be drafted.`);
|
||||
error.status = 400;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertAssignmentBase(input) {
|
||||
if (!input.assignmentTitle) {
|
||||
const error = new Error("Assignment title is required.");
|
||||
error.status = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!input.studentId) {
|
||||
const error = new Error("Student ID is required.");
|
||||
error.status = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!Array.isArray(input.questions) || input.questions.length === 0) {
|
||||
const error = new Error("At least one question is required.");
|
||||
error.status = 400;
|
||||
throw error;
|
||||
}
|
||||
|
||||
for (const question of input.questions) {
|
||||
if (!Number.isInteger(question.questionId) || question.questionId < 1) {
|
||||
const error = new Error(`Question ${question.position} needs a valid question ID.`);
|
||||
error.status = 400;
|
||||
throw error;
|
||||
}
|
||||
if (!question.prompt) {
|
||||
const error = new Error(`Question ${question.position} is missing a prompt.`);
|
||||
error.status = 400;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function callQuestionGenerator({ topic, difficulty, count }) {
|
||||
const endpoint = `${trimTrailingSlash(process.env.FINE_TUNE_BACKEND_URL)}/api/questions/generate`;
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
authorization: `Bearer ${process.env.FINE_TUNE_BACKEND_TOKEN}`,
|
||||
},
|
||||
body: JSON.stringify({ topic, difficulty, count }),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text();
|
||||
const error = new Error(`Question generator failed (${response.status}): ${errorBody}`);
|
||||
error.status = response.status;
|
||||
throw error;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
function mapGeneratedQuestion(generated, index) {
|
||||
const question = generated?.question || {};
|
||||
const workedSolution = Array.isArray(generated?.worked_solution) ? generated.worked_solution : [];
|
||||
const tags = Array.isArray(generated?.tags) ? generated.tags.filter((tag) => typeof tag === "string" && tag.trim()) : [];
|
||||
|
||||
return {
|
||||
questionId: integerOrNull(question.id),
|
||||
position: index + 1,
|
||||
title: stringOrEmpty(question.title),
|
||||
prompt: stringOrEmpty(question.prompt),
|
||||
subject: stringOrEmpty(question.subject) || "Mathematics",
|
||||
source: stringOrEmpty(question.source),
|
||||
difficulty: stringOrEmpty(question.difficulty),
|
||||
correctAnswer: stringOrEmpty(question.correct_answer),
|
||||
workedSolution: workedSolution.join("\n"),
|
||||
tags,
|
||||
studentAnswer: "",
|
||||
workingSteps: "",
|
||||
solveMode: "show_work",
|
||||
aiFeedback: "",
|
||||
understandingScore: null,
|
||||
confidence: null,
|
||||
needsAttention: null,
|
||||
issueReason: "",
|
||||
};
|
||||
}
|
||||
|
||||
function buildStudentDraftPrompt(input) {
|
||||
return [
|
||||
`Assignment ID: ${input.assignmentId || "draft-assignment"}`,
|
||||
`Assignment title: ${input.assignmentTitle}`,
|
||||
`Instructions: ${input.instructions || "No extra instructions."}`,
|
||||
`Student ID: ${input.studentId}`,
|
||||
`Pass threshold: ${typeof input.passThreshold === "number" ? input.passThreshold : "Not set"}`,
|
||||
"Generate a realistic student submission for every question below.",
|
||||
...input.questions.map((question) =>
|
||||
[
|
||||
`Question ${question.position}`,
|
||||
`questionId: ${question.questionId}`,
|
||||
`title: ${question.title || "Untitled"}`,
|
||||
`prompt: ${question.prompt}`,
|
||||
`subject: ${question.subject || "Mathematics"}`,
|
||||
`difficulty: ${question.difficulty || input.difficulty || "Not specified"}`,
|
||||
`tags: ${question.tags.join(", ") || "None"}`,
|
||||
`correctAnswer: ${question.correctAnswer}`,
|
||||
`workedSolution: ${question.workedSolution}`,
|
||||
"Return answerText, workingSteps, and solveMode for this question.",
|
||||
].join("\n"),
|
||||
),
|
||||
].join("\n\n");
|
||||
}
|
||||
|
||||
function buildTeacherDraftPrompt(input) {
|
||||
return [
|
||||
`Assignment ID: ${input.assignmentId || "draft-assignment"}`,
|
||||
`Assignment title: ${input.assignmentTitle}`,
|
||||
`Instructions: ${input.instructions || "No extra instructions."}`,
|
||||
`Student ID: ${input.studentId}`,
|
||||
`Pass threshold: ${typeof input.passThreshold === "number" ? input.passThreshold : "Not set"}`,
|
||||
"Review the full assignment in one pass. Return every question review plus an assignmentSummary and recommendedNextStep.",
|
||||
...input.questions.map((question) =>
|
||||
[
|
||||
`Question ${question.position}`,
|
||||
`questionId: ${question.questionId}`,
|
||||
`title: ${question.title || "Untitled"}`,
|
||||
`prompt: ${question.prompt}`,
|
||||
`subject: ${question.subject || "Mathematics"}`,
|
||||
`source: ${question.source || "rng_generated"}`,
|
||||
`correctAnswer: ${question.correctAnswer}`,
|
||||
`questionTags: ${question.tags.join(", ") || "None"}`,
|
||||
`solveMode: ${question.solveMode || "show_work"}`,
|
||||
`answerText: ${question.studentAnswer || "No answer provided."}`,
|
||||
`workingSteps: ${question.workingSteps || "No working shown."}`,
|
||||
`answerStatus: ${deriveAnswerStatus(question)}`,
|
||||
`isCorrect: ${deriveIsCorrect(question)}`,
|
||||
].join("\n"),
|
||||
),
|
||||
].join("\n\n");
|
||||
}
|
||||
|
||||
function normalizeStudentDraftQuestions(sourceQuestions, generatedQuestions) {
|
||||
const generatedById = new Map();
|
||||
if (Array.isArray(generatedQuestions)) {
|
||||
for (const item of generatedQuestions) {
|
||||
const questionId = integerOrNull(item?.questionId);
|
||||
if (questionId) generatedById.set(questionId, item);
|
||||
}
|
||||
}
|
||||
|
||||
return sourceQuestions.map((question, index) => {
|
||||
const draft = generatedById.get(question.questionId) || generatedQuestions?.[index] || {};
|
||||
return {
|
||||
questionId: question.questionId,
|
||||
answerText: stringOrEmpty(draft.answerText),
|
||||
workingSteps: stringOrEmpty(draft.workingSteps),
|
||||
solveMode: stringOrEmpty(draft.solveMode) || question.solveMode || "show_work",
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeTeacherDraftQuestions(sourceQuestions, generatedQuestions) {
|
||||
const generatedById = new Map();
|
||||
if (Array.isArray(generatedQuestions)) {
|
||||
for (const item of generatedQuestions) {
|
||||
const questionId = integerOrNull(item?.questionId);
|
||||
if (questionId) generatedById.set(questionId, item);
|
||||
}
|
||||
}
|
||||
|
||||
return sourceQuestions.map((question, index) => {
|
||||
const draft = generatedById.get(question.questionId) || generatedQuestions?.[index] || {};
|
||||
return {
|
||||
questionId: question.questionId,
|
||||
aiFeedback: stringOrEmpty(draft.aiFeedback),
|
||||
understandingScore: clampScore(draft.understandingScore),
|
||||
confidence: clampScore(draft.confidence),
|
||||
needsAttention: booleanOrDefault(draft.needsAttention, null),
|
||||
issueReason: stringOrEmpty(draft.issueReason),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function deriveAnswerStatus(question) {
|
||||
if (!question.studentAnswer && !question.workingSteps) return "unanswered";
|
||||
return "submitted";
|
||||
}
|
||||
|
||||
function deriveIsCorrect(question) {
|
||||
if (!question.studentAnswer) return false;
|
||||
return normalizeComparable(question.studentAnswer) === normalizeComparable(question.correctAnswer);
|
||||
}
|
||||
|
||||
function normalizeComparable(value) {
|
||||
return String(value || "")
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function stringOrEmpty(value) {
|
||||
return typeof value === "string" ? value.trim() : "";
|
||||
}
|
||||
|
||||
function stringArray(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => stringOrEmpty(item)).filter(Boolean);
|
||||
}
|
||||
if (typeof value === "string") {
|
||||
return value
|
||||
.split(",")
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function integerOrNull(value) {
|
||||
const parsed = Number.parseInt(String(value ?? "").trim(), 10);
|
||||
if (!Number.isInteger(parsed) || parsed < 1) return null;
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function integerOrDefault(value, fallback) {
|
||||
const parsed = integerOrNull(value);
|
||||
return parsed ?? fallback;
|
||||
}
|
||||
|
||||
function numberOrNull(value) {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed)) return null;
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function clampScore(value) {
|
||||
const parsed = Number(value);
|
||||
if (!Number.isFinite(parsed)) return null;
|
||||
return Math.max(0, Math.min(1, Number(parsed.toFixed(2))));
|
||||
}
|
||||
|
||||
function booleanOrNull(value) {
|
||||
if (value === true || value === "true") return true;
|
||||
if (value === false || value === "false") return false;
|
||||
return null;
|
||||
}
|
||||
|
||||
function booleanOrDefault(value, fallback) {
|
||||
const parsed = booleanOrNull(value);
|
||||
return parsed === null ? fallback : parsed;
|
||||
}
|
||||
|
||||
function trimTrailingSlash(value) {
|
||||
return String(value || "").replace(/\/+$/, "");
|
||||
}
|
||||
|
||||
async function callAiJson({ systemPrompt, userPrompt, schemaName, schema }) {
|
||||
const endpoint = process.env.FINE_TUNE_AI_ENDPOINT;
|
||||
const model = process.env.FINE_TUNE_AI_MODEL;
|
||||
const response = await fetch(endpoint, {
|
||||
method: "POST",
|
||||
headers: buildHeaders(endpoint),
|
||||
body: JSON.stringify(buildRequestBody({ endpoint, model, systemPrompt, userPrompt, schemaName, schema })),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorBody = await response.text();
|
||||
const error = new Error(`AI request failed (${response.status}): ${errorBody}`);
|
||||
error.status = response.status;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
const rawText = isChatEndpoint(endpoint) ? extractChatText(payload) : extractResponsesText(payload);
|
||||
|
||||
if (!rawText) {
|
||||
throw new Error("AI response did not include usable text output.");
|
||||
}
|
||||
|
||||
return JSON.parse(rawText);
|
||||
}
|
||||
|
||||
function buildHeaders(endpoint) {
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
};
|
||||
|
||||
if (isAzureEndpoint(endpoint)) {
|
||||
headers["api-key"] = process.env.FINE_TUNE_AI_API_KEY;
|
||||
} else {
|
||||
headers.authorization = `Bearer ${process.env.FINE_TUNE_AI_API_KEY}`;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
function buildRequestBody({ endpoint, model, systemPrompt, userPrompt, schemaName, schema }) {
|
||||
if (isChatEndpoint(endpoint)) {
|
||||
const body = {
|
||||
model,
|
||||
temperature: 0,
|
||||
messages: [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content: userPrompt },
|
||||
],
|
||||
response_format: {
|
||||
type: "json_schema",
|
||||
json_schema: {
|
||||
name: schemaName,
|
||||
strict: true,
|
||||
schema,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
if (!isAzureEndpoint(endpoint)) {
|
||||
body.chat_template_kwargs = { enable_thinking: false };
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
return {
|
||||
model,
|
||||
input: [
|
||||
{
|
||||
role: "system",
|
||||
content: [{ type: "input_text", text: systemPrompt }],
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content: [{ type: "input_text", text: userPrompt }],
|
||||
},
|
||||
],
|
||||
text: {
|
||||
format: {
|
||||
type: "json_schema",
|
||||
name: schemaName,
|
||||
strict: true,
|
||||
schema,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function isChatEndpoint(endpoint) {
|
||||
return typeof endpoint === "string" && endpoint.includes("/chat/completions");
|
||||
}
|
||||
|
||||
function isAzureEndpoint(endpoint) {
|
||||
return typeof endpoint === "string" && (endpoint.includes("cognitiveservices.azure.com") || endpoint.includes(".openai.azure.com"));
|
||||
}
|
||||
|
||||
function extractChatText(payload) {
|
||||
const content = payload?.choices?.[0]?.message?.content;
|
||||
if (typeof content === "string") return content;
|
||||
if (!Array.isArray(content)) return "";
|
||||
|
||||
return content
|
||||
.map((part) => {
|
||||
if (typeof part === "string") return part;
|
||||
if (part && typeof part.text === "string") return part.text;
|
||||
return "";
|
||||
})
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function extractResponsesText(payload) {
|
||||
if (typeof payload?.output_text === "string" && payload.output_text.trim()) {
|
||||
return payload.output_text;
|
||||
}
|
||||
|
||||
const queue = [payload];
|
||||
while (queue.length) {
|
||||
const current = queue.shift();
|
||||
if (!current || typeof current !== "object") continue;
|
||||
if (typeof current.output_text === "string" && current.output_text.trim()) return current.output_text;
|
||||
if (typeof current.text === "string" && current.text.trim()) return current.text;
|
||||
|
||||
for (const value of Object.values(current)) {
|
||||
if (Array.isArray(value)) queue.push(...value);
|
||||
else if (value && typeof value === "object") queue.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
function handleError(res, error) {
|
||||
const status = Number.isInteger(error?.status) ? error.status : 500;
|
||||
res.status(status).json({
|
||||
message: error instanceof Error ? error.message : "Unexpected error",
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user