mirror of
https://github.com/ostris/ai-toolkit.git
synced 2026-05-11 08:20:35 +00:00
Switch to uplot for loss graph and rework performance of graph. It is significantly more perfromant now.
This commit is contained in:
389
ui/package-lock.json
generated
389
ui/package-lock.json
generated
@@ -24,9 +24,9 @@
|
||||
"react-global-hooks": "^1.3.5",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-select": "^5.10.1",
|
||||
"recharts": "^3.6.0",
|
||||
"sqlite3": "^5.1.7",
|
||||
"systeminformation": "^5.27.11",
|
||||
"uplot": "^1.6.32",
|
||||
"uuid": "^11.1.0",
|
||||
"yaml": "^2.7.0"
|
||||
},
|
||||
@@ -1314,54 +1314,6 @@
|
||||
"react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@reduxjs/toolkit": {
|
||||
"version": "2.11.2",
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz",
|
||||
"integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"@standard-schema/utils": "^0.3.0",
|
||||
"immer": "^11.0.0",
|
||||
"redux": "^5.0.1",
|
||||
"redux-thunk": "^3.1.0",
|
||||
"reselect": "^5.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
|
||||
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
},
|
||||
"react-redux": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@reduxjs/toolkit/node_modules/immer": {
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-11.0.1.tgz",
|
||||
"integrity": "sha512-naDCyggtcBWANtIrjQEajhhBEuL9b0Zg4zmlWK2CzS6xCWSE39/vvf4LqnMjUAWHBhot4m9MHCM/Z+mfWhUkiA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
}
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@standard-schema/utils": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@swc/helpers": {
|
||||
"version": "0.5.15",
|
||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
||||
@@ -1438,69 +1390,6 @@
|
||||
"@types/readdir-glob": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-array": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-color": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-ease": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-interpolate": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-color": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-path": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
|
||||
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-scale": {
|
||||
"version": "4.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
||||
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-time": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-shape": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
|
||||
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-path": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-time": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
|
||||
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-timer": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
||||
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.17.19",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.19.tgz",
|
||||
@@ -1561,12 +1450,6 @@
|
||||
"integrity": "sha512-7NQmHra/JILCd1QqpSzl8+mJRc8ZHz3uDm8YV1Ks9IhK0epEiTw8aIErbvH9PI+6XbqhyIQy3462nEsn7UVzjQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/use-sync-external-store": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/abbrev": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
|
||||
@@ -2751,127 +2634,6 @@
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
||||
},
|
||||
"node_modules/d3-array": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"internmap": "1 - 2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-color": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-ease": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-format": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
|
||||
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-interpolate": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-path": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
|
||||
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-scale": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2.10.0 - 3",
|
||||
"d3-format": "1 - 3",
|
||||
"d3-interpolate": "1.2.0 - 3",
|
||||
"d3-time": "2.1.1 - 3",
|
||||
"d3-time-format": "2 - 4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-shape": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-path": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time-format": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-time": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-timer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.0",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
|
||||
@@ -2889,12 +2651,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decimal.js-light": {
|
||||
"version": "2.5.1",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
|
||||
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/decompress-response": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
@@ -3113,6 +2869,7 @@
|
||||
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.43.0.tgz",
|
||||
"integrity": "sha512-SKCT8AsWvYzBBuUqMk4NPwFlSdqLpJwmy6AP322ERn8W2YLIB6JBXnwMI2Qsh2gfphT3q7EKAxKb23cvFHFwKA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"workspaces": [
|
||||
"docs",
|
||||
"benchmarks"
|
||||
@@ -3146,12 +2903,6 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/events": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
|
||||
@@ -3684,16 +3435,6 @@
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/immer": {
|
||||
"version": "10.2.0",
|
||||
"resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz",
|
||||
"integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/immer"
|
||||
}
|
||||
},
|
||||
"node_modules/import-fresh": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
|
||||
@@ -3919,15 +3660,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/internmap": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz",
|
||||
@@ -5399,29 +5131,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||
},
|
||||
"node_modules/react-redux": {
|
||||
"version": "9.2.0",
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/use-sync-external-store": "^0.0.6",
|
||||
"use-sync-external-store": "^1.4.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.2.25 || ^19",
|
||||
"react": "^18.0 || ^19",
|
||||
"redux": "^5.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"redux": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/react-select": {
|
||||
"version": "5.10.1",
|
||||
"resolved": "https://registry.npmjs.org/react-select/-/react-select-5.10.1.tgz",
|
||||
@@ -5511,51 +5220,6 @@
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/recharts": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.6.0.tgz",
|
||||
"integrity": "sha512-L5bjxvQRAe26RlToBAziKUB7whaGKEwD3znoM6fz3DrTowCIC/FnJYnuq1GEzB8Zv2kdTfaxQfi5GoH0tBinyg==",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"www"
|
||||
],
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "1.x.x || 2.x.x",
|
||||
"clsx": "^2.1.1",
|
||||
"decimal.js-light": "^2.5.1",
|
||||
"es-toolkit": "^1.39.3",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"immer": "^10.1.1",
|
||||
"react-redux": "8.x.x || 9.x.x",
|
||||
"reselect": "5.1.1",
|
||||
"tiny-invariant": "^1.3.3",
|
||||
"use-sync-external-store": "^1.2.2",
|
||||
"victory-vendor": "^37.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/redux": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/redux-thunk": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
|
||||
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"redux": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/require-directory": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
@@ -5565,12 +5229,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/reselect": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
||||
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.10",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
|
||||
@@ -6499,12 +6157,6 @@
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-invariant": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
@@ -6755,6 +6407,12 @@
|
||||
"imurmurhash": "^0.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/uplot": {
|
||||
"version": "1.6.32",
|
||||
"resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.32.tgz",
|
||||
"integrity": "sha512-KIMVnG68zvu5XXUbC4LQEPnhwOxBuLyW1AHtpm6IKTXImkbLgkMy+jabjLgSLMasNuGGzQm/ep3tOkyTxpiQIw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/use-isomorphic-layout-effect": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.0.tgz",
|
||||
@@ -6768,15 +6426,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
@@ -6800,28 +6449,6 @@
|
||||
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/victory-vendor": {
|
||||
"version": "37.3.6",
|
||||
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
|
||||
"integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
|
||||
"license": "MIT AND ISC",
|
||||
"dependencies": {
|
||||
"@types/d3-array": "^3.0.3",
|
||||
"@types/d3-ease": "^3.0.0",
|
||||
"@types/d3-interpolate": "^3.0.1",
|
||||
"@types/d3-scale": "^4.0.2",
|
||||
"@types/d3-shape": "^3.1.0",
|
||||
"@types/d3-time": "^3.0.0",
|
||||
"@types/d3-timer": "^3.0.0",
|
||||
"d3-array": "^3.1.6",
|
||||
"d3-ease": "^3.0.1",
|
||||
"d3-interpolate": "^3.0.1",
|
||||
"d3-scale": "^4.0.2",
|
||||
"d3-shape": "^3.1.0",
|
||||
"d3-time": "^3.0.0",
|
||||
"d3-timer": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
|
||||
@@ -28,9 +28,9 @@
|
||||
"react-global-hooks": "^1.3.5",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-select": "^5.10.1",
|
||||
"recharts": "^3.6.0",
|
||||
"sqlite3": "^5.1.7",
|
||||
"systeminformation": "^5.27.11",
|
||||
"uplot": "^1.6.32",
|
||||
"uuid": "^11.1.0",
|
||||
"yaml": "^2.7.0"
|
||||
},
|
||||
|
||||
@@ -48,7 +48,7 @@ const pages: Page[] = [
|
||||
value: 'loss_log',
|
||||
icon: MdShowChart,
|
||||
component: JobLossGraph,
|
||||
mainCss: 'pt-24',
|
||||
mainCss: 'pt-24 pb-4',
|
||||
jobTypes: ['train'],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
import { Job } from '@prisma/client';
|
||||
import useJobLossLog, { LossPoint } from '@/hooks/useJobLossLog';
|
||||
import { useMemo, useState, useEffect, useCallback, useRef } from 'react';
|
||||
import { ResponsiveContainer, LineChart, Line, XAxis, YAxis, Tooltip, CartesianGrid, Legend } from 'recharts';
|
||||
import { useMemo, useState, useEffect, useRef, useCallback } from 'react';
|
||||
import uPlot from 'uplot';
|
||||
import 'uplot/dist/uPlot.min.css';
|
||||
|
||||
interface Props {
|
||||
job: Job;
|
||||
@@ -21,22 +22,36 @@ function clamp01(x: number) {
|
||||
return Math.max(0, Math.min(1, x));
|
||||
}
|
||||
|
||||
// EMA smoothing that works on a per-series list.
|
||||
// alpha=1 -> no smoothing, alpha closer to 0 -> more smoothing.
|
||||
function emaSmoothPoints(points: { step: number; value: number }[], alpha: number) {
|
||||
if (points.length === 0) return [];
|
||||
const a = clamp01(alpha);
|
||||
const out: { step: number; value: number }[] = new Array(points.length);
|
||||
// Fallback canvas height used before the container has been measured.
|
||||
const FALLBACK_CANVAS_HEIGHT = 360;
|
||||
const MIN_CANVAS_HEIGHT = 160;
|
||||
|
||||
let prev = points[0].value;
|
||||
out[0] = { step: points[0].step, value: prev };
|
||||
// Compute canvas size so uPlot's canvas + its HTML legend fit inside `host`.
|
||||
// `host` should be a layout-controlled wrapper (NOT the uPlot mount node, since
|
||||
// uPlot's stylesheet sets `width: min-content` on its mount node).
|
||||
function computeCanvasSize(host: HTMLElement): { width: number; height: number } | null {
|
||||
const { width, height } = host.getBoundingClientRect();
|
||||
if (width <= 0 || height <= 0) return null;
|
||||
const legend = host.querySelector('.u-legend') as HTMLElement | null;
|
||||
const legendH = legend?.getBoundingClientRect().height ?? 0;
|
||||
return { width, height: Math.max(MIN_CANVAS_HEIGHT, height - legendH) };
|
||||
}
|
||||
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
const x = points[i].value;
|
||||
prev = a * x + (1 - a) * prev;
|
||||
out[i] = { step: points[i].step, value: prev };
|
||||
// EMA over a (number|null)[] series. Nulls are preserved as gaps and do not
|
||||
// advance the running average.
|
||||
function emaWithNulls(ys: (number | null)[], alpha: number): (number | null)[] {
|
||||
const out: (number | null)[] = new Array(ys.length);
|
||||
let prev: number | null = null;
|
||||
for (let i = 0; i < ys.length; i++) {
|
||||
const v = ys[i];
|
||||
if (v === null || !Number.isFinite(v)) {
|
||||
out[i] = null;
|
||||
continue;
|
||||
}
|
||||
if (prev === null) prev = v as number;
|
||||
else prev = alpha * (v as number) + (1 - alpha) * prev;
|
||||
out[i] = prev;
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -64,7 +79,6 @@ function strokeForKey(key: string) {
|
||||
return PALETTE[hashToIndex(key, PALETTE.length)];
|
||||
}
|
||||
|
||||
// Returns a solid but duller/darker version of an rgba color string for the trend overlay.
|
||||
function dulledColor(rgba: string): string {
|
||||
const m = rgba.match(/rgba?\((\d+),(\d+),(\d+)/);
|
||||
if (!m) return 'rgba(120,120,120,1)';
|
||||
@@ -97,6 +111,8 @@ export default function JobLossGraph({ job }: Props) {
|
||||
// which loss series are enabled (default: all enabled)
|
||||
const [enabled, setEnabled] = useState<Record<string, boolean>>({});
|
||||
|
||||
const [isZoomed, setIsZoomed] = useState(false);
|
||||
|
||||
// keep enabled map in sync with discovered keys (enable new ones automatically)
|
||||
useEffect(() => {
|
||||
setEnabled(prev => {
|
||||
@@ -104,7 +120,6 @@ export default function JobLossGraph({ job }: Props) {
|
||||
for (const k of lossKeys) {
|
||||
if (next[k] === undefined) next[k] = true;
|
||||
}
|
||||
// drop removed keys
|
||||
for (const k of Object.keys(next)) {
|
||||
if (!lossKeys.includes(k)) delete next[k];
|
||||
}
|
||||
@@ -114,200 +129,249 @@ export default function JobLossGraph({ job }: Props) {
|
||||
|
||||
const activeKeys = useMemo(() => lossKeys.filter(k => enabled[k] !== false), [lossKeys, enabled]);
|
||||
|
||||
// Zoom state for drag-to-zoom
|
||||
const [zoomLeft, setZoomLeft] = useState<number | null>(null);
|
||||
const [zoomRight, setZoomRight] = useState<number | null>(null);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
// Selection tracked entirely in refs to avoid re-renders during drag
|
||||
const selectStartLabel = useRef<number | null>(null);
|
||||
const selectStartPx = useRef<number | null>(null);
|
||||
const overlayRef = useRef<HTMLDivElement>(null);
|
||||
const chartWrapperRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const perSeries = useMemo(() => {
|
||||
// Build per-series processed point arrays (raw + smoothed + fullSmooth), then merge by step for charting.
|
||||
// Build uPlot-aligned data + series configs.
|
||||
const built = useMemo(() => {
|
||||
const stride = Math.max(1, plotStride | 0);
|
||||
|
||||
// smoothing%: 0 => no smoothing (alpha=1.0), 100 => heavy smoothing (alpha=0.02)
|
||||
const t = clamp01(smoothing / 100);
|
||||
const alpha = 1.0 - t * 0.98; // 1.0 -> 0.02
|
||||
|
||||
// Full smoothing overlay: always max smoothing (alpha=0.02)
|
||||
const fullAlpha = 0.005;
|
||||
|
||||
const out: Record<string, { raw: { step: number; value: number }[]; smooth: { step: number; value: number }[]; fullSmooth: { step: number; value: number }[] }> =
|
||||
{};
|
||||
// Union of all steps across active series.
|
||||
const stepSet = new Set<number>();
|
||||
for (const key of activeKeys) {
|
||||
const pts: LossPoint[] = series[key] ?? [];
|
||||
for (const p of pts) {
|
||||
if (p.value === null || !Number.isFinite(p.value as number)) continue;
|
||||
if (useLogScale && (p.value as number) <= 0) continue;
|
||||
stepSet.add(p.step);
|
||||
}
|
||||
}
|
||||
let xs = Array.from(stepSet).sort((a, b) => a - b);
|
||||
if (stride > 1) xs = xs.filter((_, i) => i % stride === 0);
|
||||
if (windowSize > 0 && xs.length > windowSize) xs = xs.slice(xs.length - windowSize);
|
||||
|
||||
const xsSet = new Set(xs);
|
||||
|
||||
const data: (number[] | (number | null)[])[] = [xs];
|
||||
const seriesConfigs: uPlot.Series[] = [{}]; // x
|
||||
|
||||
for (const key of activeKeys) {
|
||||
const pts: LossPoint[] = series[key] ?? [];
|
||||
|
||||
let raw = pts
|
||||
.filter(p => p.value !== null && Number.isFinite(p.value as number))
|
||||
.map(p => ({ step: p.step, value: p.value as number }))
|
||||
.filter(p => (useLogScale ? p.value > 0 : true))
|
||||
.filter((_, idx) => idx % stride === 0);
|
||||
|
||||
// windowing (applies after stride)
|
||||
if (windowSize > 0 && raw.length > windowSize) {
|
||||
raw = raw.slice(raw.length - windowSize);
|
||||
const map = new Map<number, number>();
|
||||
for (const p of pts) {
|
||||
if (p.value === null || !Number.isFinite(p.value as number)) continue;
|
||||
if (useLogScale && (p.value as number) <= 0) continue;
|
||||
if (!xsSet.has(p.step)) continue;
|
||||
map.set(p.step, p.value as number);
|
||||
}
|
||||
const raw: (number | null)[] = xs.map(s => (map.has(s) ? (map.get(s) as number) : null));
|
||||
const smooth = emaWithNulls(raw, alpha);
|
||||
const fullSmooth = emaWithNulls(raw, fullAlpha);
|
||||
|
||||
const smooth = emaSmoothPoints(raw, alpha);
|
||||
const fullSmooth = emaSmoothPoints(raw, fullAlpha);
|
||||
const color = strokeForKey(key);
|
||||
const colorFaded = color.replace('1)', '0.40)');
|
||||
const colorDull = dulledColor(color);
|
||||
|
||||
out[key] = { raw, smooth, fullSmooth };
|
||||
if (showRaw) {
|
||||
data.push(raw);
|
||||
seriesConfigs.push({
|
||||
label: `${key} (raw)`,
|
||||
stroke: colorFaded,
|
||||
width: 1.25,
|
||||
spanGaps: false,
|
||||
points: { show: false },
|
||||
});
|
||||
}
|
||||
if (showSmoothed) {
|
||||
data.push(smooth);
|
||||
seriesConfigs.push({
|
||||
label: key,
|
||||
stroke: color,
|
||||
width: 2,
|
||||
spanGaps: false,
|
||||
points: { show: false },
|
||||
});
|
||||
}
|
||||
data.push(fullSmooth);
|
||||
seriesConfigs.push({
|
||||
label: `${key} (trend)`,
|
||||
stroke: colorDull,
|
||||
width: 2.5,
|
||||
spanGaps: false,
|
||||
points: { show: false },
|
||||
});
|
||||
}
|
||||
|
||||
return out;
|
||||
}, [series, activeKeys, smoothing, plotStride, windowSize, useLogScale]);
|
||||
|
||||
const chartData = useMemo(() => {
|
||||
// Merge series into one array of objects keyed by step.
|
||||
// Fields: `${key}__raw` and `${key}__smooth`
|
||||
const map = new Map<number, any>();
|
||||
|
||||
for (const key of activeKeys) {
|
||||
const s = perSeries[key];
|
||||
if (!s) continue;
|
||||
|
||||
for (const p of s.raw) {
|
||||
const row = map.get(p.step) ?? { step: p.step };
|
||||
row[`${key}__raw`] = p.value;
|
||||
map.set(p.step, row);
|
||||
// y-domain clipping (2nd–98th percentile of all visible y values).
|
||||
let yClip: { min: number; max: number } | null = null;
|
||||
if (clipOutliers && xs.length >= 10) {
|
||||
const vals: number[] = [];
|
||||
for (let s = 1; s < data.length; s++) {
|
||||
const arr = data[s] as (number | null)[];
|
||||
for (const v of arr) {
|
||||
if (v !== null && Number.isFinite(v)) vals.push(v as number);
|
||||
}
|
||||
}
|
||||
for (const p of s.smooth) {
|
||||
const row = map.get(p.step) ?? { step: p.step };
|
||||
row[`${key}__smooth`] = p.value;
|
||||
map.set(p.step, row);
|
||||
}
|
||||
for (const p of s.fullSmooth) {
|
||||
const row = map.get(p.step) ?? { step: p.step };
|
||||
row[`${key}__fullsmooth`] = p.value;
|
||||
map.set(p.step, row);
|
||||
if (vals.length >= 10) {
|
||||
vals.sort((a, b) => a - b);
|
||||
const lo = vals[Math.floor(vals.length * 0.02)];
|
||||
const hi = vals[Math.ceil(vals.length * 0.98) - 1];
|
||||
if (Number.isFinite(lo) && Number.isFinite(hi) && lo !== hi) {
|
||||
yClip = { min: lo, max: hi };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const arr = Array.from(map.values());
|
||||
arr.sort((a, b) => a.step - b.step);
|
||||
return arr;
|
||||
}, [activeKeys, perSeries]);
|
||||
return { data: data as uPlot.AlignedData, seriesConfigs, yClip };
|
||||
}, [series, activeKeys, smoothing, plotStride, windowSize, useLogScale, showRaw, showSmoothed, clipOutliers]);
|
||||
|
||||
// Zoomed slice of chartData
|
||||
const visibleData = useMemo(() => {
|
||||
if (zoomLeft == null || zoomRight == null) return chartData;
|
||||
const lo = Math.min(zoomLeft, zoomRight);
|
||||
const hi = Math.max(zoomLeft, zoomRight);
|
||||
return chartData.filter(d => d.step >= lo && d.step <= hi);
|
||||
}, [chartData, zoomLeft, zoomRight]);
|
||||
// Layout wrapper we measure for sizing — uPlot collapses its own mount node
|
||||
// to width:min-content, so we can't read sizes off it.
|
||||
const chartHostRef = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const uplotRef = useRef<uPlot | null>(null);
|
||||
|
||||
// Convert a pixel x within the wrapper to a fractional position [0,1] across the plot area
|
||||
const pxToFraction = useCallback((clientX: number) => {
|
||||
const wrapper = chartWrapperRef.current;
|
||||
if (!wrapper) return 0;
|
||||
const rect = wrapper.getBoundingClientRect();
|
||||
// chart margin left (8) + yAxis width (72) = 80, margin right = 16
|
||||
const plotLeft = 80;
|
||||
const plotRight = rect.width - 16;
|
||||
const plotWidth = plotRight - plotLeft;
|
||||
const localX = clientX - rect.left;
|
||||
return Math.max(0, Math.min(1, (localX - plotLeft) / plotWidth));
|
||||
}, []);
|
||||
|
||||
const fractionToStep = useCallback((frac: number) => {
|
||||
const data = visibleData;
|
||||
if (data.length === 0) return 0;
|
||||
const idx = Math.round(frac * (data.length - 1));
|
||||
return data[Math.max(0, Math.min(data.length - 1, idx))].step;
|
||||
}, [visibleData]);
|
||||
|
||||
// Native DOM events for drag selection
|
||||
// Latest yClip read by the y-scale range fn — kept current via effect.
|
||||
const yClipRef = useRef<{ min: number; max: number } | null>(null);
|
||||
useEffect(() => {
|
||||
const wrapper = chartWrapperRef.current;
|
||||
if (!wrapper) return;
|
||||
yClipRef.current = built.yClip;
|
||||
}, [built.yClip]);
|
||||
|
||||
const onDown = (e: MouseEvent) => {
|
||||
selectStartPx.current = e.clientX;
|
||||
selectStartLabel.current = fractionToStep(pxToFraction(e.clientX));
|
||||
setIsDragging(true);
|
||||
if (overlayRef.current) overlayRef.current.style.display = 'none';
|
||||
// Track zoom state via ref so the data-update effect can decide whether to refit scales.
|
||||
const isZoomedRef = useRef(false);
|
||||
useEffect(() => {
|
||||
isZoomedRef.current = isZoomed;
|
||||
}, [isZoomed]);
|
||||
|
||||
// Structural recreate key — recreate uPlot only when the series shape or
|
||||
// axis distribution changes. Data updates go through setData.
|
||||
const hasData = (built.data[0]?.length ?? 0) > 1;
|
||||
const structuralKey = useMemo(
|
||||
() => `${activeKeys.join('|')}|raw=${showRaw}|sm=${showSmoothed}|log=${useLogScale}|has=${hasData}`,
|
||||
[activeKeys, showRaw, showSmoothed, useLogScale, hasData],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (uplotRef.current) {
|
||||
uplotRef.current.destroy();
|
||||
uplotRef.current = null;
|
||||
}
|
||||
if (!containerRef.current || !chartHostRef.current) return;
|
||||
if (!hasData) return;
|
||||
|
||||
const host = chartHostRef.current;
|
||||
const rect = host.getBoundingClientRect();
|
||||
const initialHeight = rect.height > 0 ? Math.max(MIN_CANVAS_HEIGHT, rect.height - 40) : FALLBACK_CANVAS_HEIGHT;
|
||||
const opts: uPlot.Options = {
|
||||
width: rect.width || 800,
|
||||
height: initialHeight,
|
||||
padding: [12, 16, 0, 4],
|
||||
series: built.seriesConfigs,
|
||||
scales: {
|
||||
x: { time: false },
|
||||
y: {
|
||||
distr: useLogScale ? 3 : 1,
|
||||
range: (_u, dataMin, dataMax) => {
|
||||
const c = yClipRef.current;
|
||||
if (c) return [c.min, c.max];
|
||||
return [dataMin, dataMax];
|
||||
},
|
||||
},
|
||||
},
|
||||
axes: [
|
||||
{
|
||||
stroke: 'rgba(255,255,255,0.55)',
|
||||
grid: { stroke: 'rgba(255,255,255,0.06)' },
|
||||
ticks: { stroke: 'rgba(255,255,255,0.15)' },
|
||||
},
|
||||
{
|
||||
stroke: 'rgba(255,255,255,0.55)',
|
||||
grid: { stroke: 'rgba(255,255,255,0.06)' },
|
||||
ticks: { stroke: 'rgba(255,255,255,0.15)' },
|
||||
size: 60,
|
||||
values: (_u, ticks) => ticks.map(tk => formatNum(tk)),
|
||||
},
|
||||
],
|
||||
cursor: {
|
||||
drag: { x: true, y: false, setScale: true },
|
||||
points: { size: 6 },
|
||||
},
|
||||
legend: { show: true },
|
||||
hooks: {
|
||||
setScale: [
|
||||
(u, key) => {
|
||||
if (key !== 'x') return;
|
||||
const xs = u.data[0] as number[];
|
||||
if (!xs || !xs.length) return;
|
||||
const sx = u.scales.x;
|
||||
const zoomed = sx.min !== xs[0] || sx.max !== xs[xs.length - 1];
|
||||
setIsZoomed(zoomed);
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const onMove = (e: MouseEvent) => {
|
||||
if (selectStartPx.current == null) return;
|
||||
const rect = wrapper.getBoundingClientRect();
|
||||
const plotLeft = 80;
|
||||
const plotRight = rect.width - 16;
|
||||
const startLocal = selectStartPx.current - rect.left;
|
||||
const curLocal = e.clientX - rect.left;
|
||||
// Clamp to plot area
|
||||
const clampedStart = Math.max(plotLeft, Math.min(plotRight, startLocal));
|
||||
const clampedCur = Math.max(plotLeft, Math.min(plotRight, curLocal));
|
||||
const left = Math.min(clampedStart, clampedCur);
|
||||
const width = Math.abs(clampedCur - clampedStart);
|
||||
if (overlayRef.current) {
|
||||
overlayRef.current.style.display = width > 3 ? 'block' : 'none';
|
||||
overlayRef.current.style.left = `${left}px`;
|
||||
overlayRef.current.style.width = `${width}px`;
|
||||
}
|
||||
};
|
||||
uplotRef.current = new uPlot(opts, built.data, containerRef.current);
|
||||
setIsZoomed(false);
|
||||
|
||||
const onUp = (e: MouseEvent) => {
|
||||
if (selectStartPx.current == null) return;
|
||||
const startStep = selectStartLabel.current!;
|
||||
const endStep = fractionToStep(pxToFraction(e.clientX));
|
||||
selectStartPx.current = null;
|
||||
selectStartLabel.current = null;
|
||||
setIsDragging(false);
|
||||
if (overlayRef.current) overlayRef.current.style.display = 'none';
|
||||
if (startStep !== endStep) {
|
||||
setZoomLeft(Math.min(startStep, endStep));
|
||||
setZoomRight(Math.max(startStep, endStep));
|
||||
}
|
||||
};
|
||||
// After uPlot mounts its legend, right-size the canvas against the actual
|
||||
// legend height so the canvas fills the remaining vertical space.
|
||||
const fitted = computeCanvasSize(host);
|
||||
if (fitted) uplotRef.current.setSize(fitted);
|
||||
|
||||
wrapper.addEventListener('mousedown', onDown);
|
||||
window.addEventListener('mousemove', onMove);
|
||||
window.addEventListener('mouseup', onUp);
|
||||
return () => {
|
||||
wrapper.removeEventListener('mousedown', onDown);
|
||||
window.removeEventListener('mousemove', onMove);
|
||||
window.removeEventListener('mouseup', onUp);
|
||||
uplotRef.current?.destroy();
|
||||
uplotRef.current = null;
|
||||
};
|
||||
}, [pxToFraction, fractionToStep]);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [structuralKey]);
|
||||
|
||||
// Push new data without recreating — preserves zoom & cursor state.
|
||||
useEffect(() => {
|
||||
const u = uplotRef.current;
|
||||
if (!u) return;
|
||||
// When zoomed, pass resetScales=false so the user's view stays put. uPlot
|
||||
// skips its commit() in that branch though, so force a redraw to actually
|
||||
// re-render the new smoothed/strided values within the zoom window.
|
||||
if (isZoomedRef.current) {
|
||||
u.setData(built.data, false);
|
||||
u.redraw(true, true);
|
||||
} else {
|
||||
u.setData(built.data, true);
|
||||
}
|
||||
}, [built]);
|
||||
|
||||
// Resize observer — fit canvas to the wrapper's available space minus the
|
||||
// HTML legend uPlot renders below it. Observe the layout wrapper, not the
|
||||
// uPlot mount node (which uPlot pins to width:min-content).
|
||||
// Re-runs on `hasData` because the observed element only exists once data
|
||||
// has loaded (see the `!hasData` branch below).
|
||||
useEffect(() => {
|
||||
const el = chartHostRef.current;
|
||||
if (!el) return;
|
||||
const ro = new ResizeObserver(() => {
|
||||
const u = uplotRef.current;
|
||||
if (!u) return;
|
||||
const fitted = computeCanvasSize(el);
|
||||
if (fitted) u.setSize(fitted);
|
||||
});
|
||||
ro.observe(el);
|
||||
return () => ro.disconnect();
|
||||
}, [hasData]);
|
||||
|
||||
const handleResetZoom = useCallback(() => {
|
||||
setZoomLeft(null);
|
||||
setZoomRight(null);
|
||||
const u = uplotRef.current;
|
||||
if (!u) return;
|
||||
const xs = u.data[0] as number[];
|
||||
if (!xs || !xs.length) return;
|
||||
u.setScale('x', { min: xs[0], max: xs[xs.length - 1] });
|
||||
}, []);
|
||||
|
||||
const hasData = chartData.length > 1;
|
||||
const isZoomed = zoomLeft != null && zoomRight != null;
|
||||
|
||||
const yDomain = useMemo((): [number | 'auto', number | 'auto'] => {
|
||||
if (!clipOutliers || chartData.length < 10) return ['auto', 'auto'];
|
||||
|
||||
// Collect visible values (prefer smoothed if shown, else raw)
|
||||
const vals: number[] = [];
|
||||
for (const row of chartData) {
|
||||
for (const key of activeKeys) {
|
||||
const k = showSmoothed ? `${key}__smooth` : `${key}__raw`;
|
||||
const v = row[k];
|
||||
if (typeof v === 'number' && Number.isFinite(v)) vals.push(v);
|
||||
}
|
||||
}
|
||||
if (vals.length < 10) return ['auto', 'auto'];
|
||||
|
||||
vals.sort((a, b) => a - b);
|
||||
const lo = vals[Math.floor(vals.length * 0.02)];
|
||||
const hi = vals[Math.ceil(vals.length * 0.98) - 1];
|
||||
|
||||
if (!Number.isFinite(lo) || !Number.isFinite(hi) || lo === hi) return ['auto', 'auto'];
|
||||
return [lo, hi];
|
||||
}, [clipOutliers, chartData, activeKeys, showSmoothed]);
|
||||
const totalPoints = built.data[0]?.length ?? 0;
|
||||
|
||||
return (
|
||||
<div className="bg-gray-900 rounded-xl shadow-lg overflow-hidden border border-gray-800 flex flex-col">
|
||||
<div className="bg-gray-800 px-4 py-3 flex items-center justify-between">
|
||||
<div className="bg-gray-900 rounded-xl shadow-lg overflow-hidden border border-gray-800 flex flex-col h-full">
|
||||
<div className="bg-gray-800 px-4 py-3 flex items-center justify-between shrink-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="h-2 w-2 rounded-full bg-blue-400" />
|
||||
<h2 className="text-gray-100 text-sm font-medium">Loss graph</h2>
|
||||
@@ -315,7 +379,7 @@ export default function JobLossGraph({ job }: Props) {
|
||||
{status === 'loading' && 'Loading...'}
|
||||
{status === 'refreshing' && 'Refreshing...'}
|
||||
{status === 'error' && 'Error'}
|
||||
{status === 'success' && hasData && `${chartData.length.toLocaleString()} steps`}
|
||||
{status === 'success' && hasData && `${totalPoints.toLocaleString()} steps`}
|
||||
{status === 'success' && !hasData && 'No data yet'}
|
||||
</span>
|
||||
</div>
|
||||
@@ -330,126 +394,36 @@ export default function JobLossGraph({ job }: Props) {
|
||||
</div>
|
||||
|
||||
{/* Chart */}
|
||||
<div className="px-4 pt-4 pb-4">
|
||||
<div ref={chartWrapperRef} className="bg-gray-950 rounded-lg border border-gray-800 h-96 relative select-none">
|
||||
{/* Drag selection overlay — positioned via refs, no re-renders */}
|
||||
<div
|
||||
ref={overlayRef}
|
||||
style={{ display: 'none', position: 'absolute', top: 10, bottom: 10, pointerEvents: 'none', background: 'rgba(59,130,246,0.15)', border: '1px solid rgba(59,130,246,0.4)', zIndex: 5 }}
|
||||
/>
|
||||
<div className="px-4 pt-4 pb-4 flex-1 min-h-0 flex flex-col">
|
||||
<div
|
||||
className="bg-gray-950 rounded-lg border border-gray-800 relative select-none flex-1 min-h-0"
|
||||
style={{ minHeight: 240 }}
|
||||
>
|
||||
{!hasData ? (
|
||||
<div className="h-full w-full flex items-center justify-center text-sm text-gray-400">
|
||||
<div className="absolute inset-0 flex items-center justify-center text-sm text-gray-400">
|
||||
{status === 'error' ? 'Failed to load loss logs.' : 'Waiting for loss points...'}
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{isZoomed && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleResetZoom}
|
||||
className="absolute top-2 right-2 z-10 px-2 py-1 rounded text-xs bg-blue-600/80 hover:bg-blue-600 text-white border border-blue-500/50"
|
||||
>
|
||||
Reset zoom
|
||||
</button>
|
||||
)}
|
||||
<ResponsiveContainer width="100%" height="100%" style={isDragging ? { pointerEvents: 'none' } : undefined}>
|
||||
<LineChart
|
||||
data={visibleData}
|
||||
margin={{ top: 10, right: 16, bottom: 10, left: 8 }}
|
||||
>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="rgba(255,255,255,0.06)" />
|
||||
<XAxis
|
||||
dataKey="step"
|
||||
tick={{ fill: 'rgba(255,255,255,0.55)', fontSize: 12 }}
|
||||
tickLine={{ stroke: 'rgba(255,255,255,0.15)' }}
|
||||
axisLine={{ stroke: 'rgba(255,255,255,0.15)' }}
|
||||
minTickGap={40}
|
||||
/>
|
||||
<YAxis
|
||||
scale={useLogScale ? 'log' : 'linear'}
|
||||
tick={{ fill: 'rgba(255,255,255,0.55)', fontSize: 12 }}
|
||||
tickLine={{ stroke: 'rgba(255,255,255,0.15)' }}
|
||||
axisLine={{ stroke: 'rgba(255,255,255,0.15)' }}
|
||||
width={72}
|
||||
tickFormatter={formatNum}
|
||||
domain={yDomain}
|
||||
allowDataOverflow={clipOutliers}
|
||||
/>
|
||||
{!isDragging && (
|
||||
<Tooltip
|
||||
cursor={{ stroke: 'rgba(59,130,246,0.25)', strokeWidth: 1 }}
|
||||
contentStyle={{
|
||||
background: 'rgba(17,24,39,0.96)',
|
||||
border: '1px solid rgba(31,41,55,1)',
|
||||
borderRadius: 10,
|
||||
color: 'rgba(255,255,255,0.9)',
|
||||
fontSize: 12,
|
||||
}}
|
||||
labelStyle={{ color: 'rgba(255,255,255,0.75)' }}
|
||||
labelFormatter={(label: any) => `step ${label}`}
|
||||
formatter={(value: any, name: any) => [formatNum(Number(value)), name]}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Legend
|
||||
wrapperStyle={{
|
||||
paddingTop: 8,
|
||||
color: 'rgba(255,255,255,0.7)',
|
||||
fontSize: 12,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Raw lines */}
|
||||
{showRaw && activeKeys.map(k => (
|
||||
<Line
|
||||
key={`${k}__raw`}
|
||||
type="monotone"
|
||||
dataKey={`${k}__raw`}
|
||||
name={`${k} (raw)`}
|
||||
stroke={strokeForKey(k).replace('1)', '0.40)')}
|
||||
strokeWidth={1.25}
|
||||
dot={false}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
))}
|
||||
{/* Smoothed lines */}
|
||||
{showSmoothed && activeKeys.map(k => (
|
||||
<Line
|
||||
key={`${k}__smooth`}
|
||||
type="monotone"
|
||||
dataKey={`${k}__smooth`}
|
||||
name={`${k}`}
|
||||
stroke={strokeForKey(k)}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
isAnimationActive={false}
|
||||
/>
|
||||
))}
|
||||
{/* Full-smooth trend overlay — rendered last so it's on top, hidden from legend/tooltip */}
|
||||
{activeKeys.map(k => (
|
||||
<Line
|
||||
key={`${k}__fullsmooth`}
|
||||
type="monotone"
|
||||
dataKey={`${k}__fullsmooth`}
|
||||
name={`${k}__fullsmooth`}
|
||||
stroke={dulledColor(strokeForKey(k))}
|
||||
strokeWidth={2.5}
|
||||
dot={false}
|
||||
isAnimationActive={false}
|
||||
legendType="none"
|
||||
tooltipType="none"
|
||||
/>
|
||||
))}
|
||||
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
{isZoomed && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleResetZoom}
|
||||
className="absolute top-2 right-2 z-10 px-2 py-1 rounded text-xs bg-blue-600/80 hover:bg-blue-600 text-white border border-blue-500/50"
|
||||
>
|
||||
Reset zoom
|
||||
</button>
|
||||
)}
|
||||
<div ref={chartHostRef} className="absolute top-0 left-0 right-0 bottom-2 overflow-hidden">
|
||||
<div ref={containerRef} />
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
<div className="px-4 pb-2">
|
||||
<div className="px-4 pb-2 shrink-0">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div className="bg-gray-950 border border-gray-800 rounded-lg p-3">
|
||||
<label className="block text-xs text-gray-400 mb-2">Display</label>
|
||||
@@ -520,9 +494,31 @@ export default function JobLossGraph({ job }: Props) {
|
||||
/>
|
||||
<div className="mt-2 text-[11px] text-gray-500">UI downsample for huge runs.</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style jsx global>{`
|
||||
.uplot,
|
||||
.uplot * {
|
||||
font-family: inherit;
|
||||
}
|
||||
.uplot .u-legend {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.uplot .u-legend th,
|
||||
.uplot .u-legend td {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
.uplot .u-legend .u-marker {
|
||||
border-radius: 2px;
|
||||
}
|
||||
.uplot .u-select {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
border: 1px solid rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
1
ui/src/globals.d.ts
vendored
Normal file
1
ui/src/globals.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
declare module '*.css';
|
||||
@@ -1 +1 @@
|
||||
VERSION = "0.9.7"
|
||||
VERSION = "0.9.8"
|
||||
|
||||
Reference in New Issue
Block a user