From b2df1a1edcda55e4617704e332041fd9c168c51c Mon Sep 17 00:00:00 2001 From: Brent Salisbury Date: Mon, 9 Dec 2024 01:24:31 -0500 Subject: [PATCH 1/4] Fine-tuning and post train chat eval POC - Demonstrates an end to end knowledge submission, generate, train and post-train side-by-side model comparison for the user to validate their knowledge submission is included in the newly trained checkpoint. - For this to function for the frontend to make REST API calls to Instructlab this uses an api-server that frontends ilab. The code is here https://github.com/nerdalert/ilab-api-server - The demo was run on a 24GB GPU leveraging the simple pipeline. Will get an example acceslerated pipeline demo with some hardware soon. - Training and generation for the demo took around ~30-45m or so. - All functionality is decoupled from the system via REST making it serviceable out of the gate and enabling the UI functionality. Signed-off-by: Brent Salisbury --- package-lock.json | 2672 ++++++++++++++--- package.json | 4 + src/app/api/fine-tune/data-sets/route.ts | 22 + src/app/api/fine-tune/data/generate/route.ts | 26 + .../api/fine-tune/jobs/[job_id]/logs/route.ts | 29 + .../fine-tune/jobs/[job_id]/status/route.ts | 29 + src/app/api/fine-tune/jobs/route.ts | 22 + .../api/fine-tune/model/serve-base/route.ts | 54 + .../api/fine-tune/model/serve-latest/route.ts | 54 + src/app/api/fine-tune/model/train/route.ts | 70 + src/app/api/fine-tune/models/route.ts | 22 + .../pipeline/generate-train/route.ts | 40 + src/app/experimental/chat-eval/page.tsx | 17 + src/app/experimental/fine-tune/page.tsx | 17 + src/components/AppLayout.tsx | 5 +- .../Experimental/ChatEval/ChatEval.tsx | 666 ++++ .../Experimental/FineTuning/index.tsx | 469 +++ src/components/app.scss | 276 ++ src/types/index.ts | 2 +- 19 files changed, 4032 insertions(+), 464 deletions(-) create mode 100644 src/app/api/fine-tune/data-sets/route.ts create mode 100644 src/app/api/fine-tune/data/generate/route.ts create mode 100644 src/app/api/fine-tune/jobs/[job_id]/logs/route.ts create mode 100644 src/app/api/fine-tune/jobs/[job_id]/status/route.ts create mode 100644 src/app/api/fine-tune/jobs/route.ts create mode 100644 src/app/api/fine-tune/model/serve-base/route.ts create mode 100644 src/app/api/fine-tune/model/serve-latest/route.ts create mode 100644 src/app/api/fine-tune/model/train/route.ts create mode 100644 src/app/api/fine-tune/models/route.ts create mode 100644 src/app/api/fine-tune/pipeline/generate-train/route.ts create mode 100644 src/app/experimental/chat-eval/page.tsx create mode 100644 src/app/experimental/fine-tune/page.tsx create mode 100644 src/components/Experimental/ChatEval/ChatEval.tsx create mode 100644 src/components/Experimental/FineTuning/index.tsx create mode 100644 src/components/app.scss diff --git a/package-lock.json b/package-lock.json index 97830d4f..6f2621ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,11 +11,15 @@ "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.7.1", "@next/env": "^15.0.3", + "@patternfly/chatbot": "^2.1.0-prerelease.17", "@patternfly/react-core": "^6.0.0", "@patternfly/react-icons": "^6.0.0", "@patternfly/react-styles": "^6.0.0", "@patternfly/react-table": "^6.0.0", + "@patternfly/virtual-assistant": "^2.0.2", "axios": "^1.7.9", + "date-fns": "^4.1.0", + "dompurify": "^3.2.2", "fs": "^0.0.1-security", "isomorphic-git": "^1.27.2", "js-yaml": "^4.1.0", @@ -343,15 +347,19 @@ "kuler": "^2.0.0" } }, - "node_modules/@emnapi/runtime": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.3.1.tgz", - "integrity": "sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==", - "optional": true, + "node_modules/@emotion/is-prop-valid": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.7.3.tgz", + "integrity": "sha512-uxJqm/sqwXw3YPA5GXX365OBcJGFtxUVkB6WyezqFHlNe9jqUWH5ur2O2M8dGBz61kn1g3ZBlzUunFQXQIClhA==", "dependencies": { - "tslib": "^2.4.0" + "@emotion/memoize": "0.7.1" } }, + "node_modules/@emotion/memoize": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.1.tgz", + "integrity": "sha512-Qv4LTqO11jepd5Qmlp3M1YEjBumoTHcHFdgPTQ+sFlIL5myi/7xu/POwP7IRu6odBdmLXdtIs1D6TuW6kbwbbg==" + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -469,7 +477,6 @@ "version": "6.7.1", "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.7.1.tgz", "integrity": "sha512-gbDz3TwRrIPT3i0cDfujhshnXO9z03IT1UKRIVi/VEjpNHtSBIP2o5XSm+e816FzzCFEzAxPw09Z13n20PaQJQ==", - "license": "MIT", "engines": { "node": ">=6" } @@ -490,7 +497,6 @@ "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.1.tgz", "integrity": "sha512-BTKc0b0mgjWZ2UDKVgmwaE0qt0cZs6ITcDgjrti5f/ki7aF5zs+N91V6hitGo3TItCFtnKg6cUVGdTmBFICFRg==", "dev": true, - "license": "(CC-BY-4.0 AND MIT)", "dependencies": { "@fortawesome/fontawesome-common-types": "6.7.1" }, @@ -594,27 +600,6 @@ "@img/sharp-libvips-darwin-arm64": "1.0.4" } }, - "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", - "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.4" - } - }, "node_modules/@img/sharp-libvips-darwin-arm64": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", @@ -630,291 +615,6 @@ "url": "https://opencollective.com/libvips" } }, - "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", - "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", - "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", - "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", - "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", - "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", - "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", - "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-linux-arm": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", - "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.5" - } - }, - "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", - "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", - "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", - "cpu": [ - "s390x" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.4" - } - }, - "node_modules/@img/sharp-linux-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", - "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", - "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" - } - }, - "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", - "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.4" - } - }, - "node_modules/@img/sharp-wasm32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", - "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", - "cpu": [ - "wasm32" - ], - "optional": true, - "dependencies": { - "@emnapi/runtime": "^1.2.0" - }, - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", - "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, - "node_modules/@img/sharp-win32-x64": { - "version": "0.33.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", - "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1015,6 +715,30 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@monaco-editor/loader": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/loader/-/loader-1.4.0.tgz", + "integrity": "sha512-00ioBig0x642hytVspPl7DbQyaSWRaolYie/UFNjoTdvoKPzo6xrXLhTk9ixgIKcLH5b5vDOjVNiGyY+uDCUlg==", + "dependencies": { + "state-local": "^1.0.6" + }, + "peerDependencies": { + "monaco-editor": ">= 0.21.0 < 1" + } + }, + "node_modules/@monaco-editor/react": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@monaco-editor/react/-/react-4.6.0.tgz", + "integrity": "sha512-RFkU9/i7cN2bsq/iTkurMWOEErmYcY6JiQI3Jn+WeR/FGISH8JbHERjpS9oRuSOPvDMJI0Z8nJeKkbOs9sBYQw==", + "dependencies": { + "@monaco-editor/loader": "^1.4.0" + }, + "peerDependencies": { + "monaco-editor": ">= 0.25.0 < 1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/@next/env": { "version": "15.0.3", "resolved": "https://registry.npmjs.org/@next/env/-/env-15.0.3.tgz", @@ -1045,119 +769,14 @@ "node": ">= 10" } }, - "node_modules/@next/swc-darwin-x64": { - "version": "15.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.0.3.tgz", - "integrity": "sha512-Zxl/TwyXVZPCFSf0u2BNj5sE0F2uR6iSKxWpq4Wlk/Sv9Ob6YCKByQTkV2y6BCic+fkabp9190hyrDdPA/dNrw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.0.3.tgz", - "integrity": "sha512-T5+gg2EwpsY3OoaLxUIofmMb7ohAUlcNZW0fPQ6YAutaWJaxt1Z1h+8zdl4FRIOr5ABAAhXtBcpkZNwUcKI2fw==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.0.3.tgz", - "integrity": "sha512-WkAk6R60mwDjH4lG/JBpb2xHl2/0Vj0ZRu1TIzWuOYfQ9tt9NFsIinI1Epma77JVgy81F32X/AeD+B2cBu/YQA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.0.3.tgz", - "integrity": "sha512-gWL/Cta1aPVqIGgDb6nxkqy06DkwJ9gAnKORdHWX1QBbSZZB+biFYPFti8aKIQL7otCE1pjyPaXpFzGeG2OS2w==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "15.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.0.3.tgz", - "integrity": "sha512-QQEMwFd8r7C0GxQS62Zcdy6GKx999I/rTO2ubdXEe+MlZk9ZiinsrjwoiBL5/57tfyjikgh6GOU2WRQVUej3UA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.0.3.tgz", - "integrity": "sha512-9TEp47AAd/ms9fPNgtgnT7F3M1Hf7koIYYWCMQ9neOwjbVWJsHZxrFbI3iEDJ8rf1TDGpmHbKxXf2IFpAvheIQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.0.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.0.3.tgz", - "integrity": "sha512-VNAz+HN4OGgvZs6MOoVfnn41kBzT+M+tB+OK4cww6DNyWS6wKaDpaAm/qLeOUbnMh0oVx1+mg0uoYARF69dJyA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, - "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { - "version": "5.1.1-v1", - "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", - "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-scope": "5.1.1" + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", + "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-scope": "5.1.1" } }, "node_modules/@nodelib/fs.scandir": { @@ -1207,6 +826,114 @@ "url": "https://github.com/sponsors/panva" } }, + "node_modules/@parcel/watcher": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz", + "integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==", + "hasInstallScript": true, + "optional": true, + "peer": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.0", + "@parcel/watcher-darwin-arm64": "2.5.0", + "@parcel/watcher-darwin-x64": "2.5.0", + "@parcel/watcher-freebsd-x64": "2.5.0", + "@parcel/watcher-linux-arm-glibc": "2.5.0", + "@parcel/watcher-linux-arm-musl": "2.5.0", + "@parcel/watcher-linux-arm64-glibc": "2.5.0", + "@parcel/watcher-linux-arm64-musl": "2.5.0", + "@parcel/watcher-linux-x64-glibc": "2.5.0", + "@parcel/watcher-linux-x64-musl": "2.5.0", + "@parcel/watcher-win32-arm64": "2.5.0", + "@parcel/watcher-win32-ia32": "2.5.0", + "@parcel/watcher-win32-x64": "2.5.0" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz", + "integrity": "sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "optional": true, + "peer": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/@patternfly/chatbot": { + "version": "2.1.0-prerelease.17", + "resolved": "https://registry.npmjs.org/@patternfly/chatbot/-/chatbot-2.1.0-prerelease.17.tgz", + "integrity": "sha512-fped4uypC7pci4jUgjguSo04v6h4YTow7x3Z9rU5+AjHfvE98vf7H7YAskd8wrhZOvjITHxhNiFGIZvQm4MViw==", + "dependencies": { + "@patternfly/react-code-editor": "^6.0.0", + "@patternfly/react-core": "^6.0.0", + "@patternfly/react-icons": "^6.0.0", + "clsx": "^2.1.0", + "framer-motion": "^11.3.28", + "path-browserify": "^1.0.1", + "react-jss": "^10.10.0", + "react-markdown": "^9.0.1", + "react-syntax-highlighter": "^15.5.0", + "react-textarea-auto-witdth-height": "^1.0.3", + "remark-gfm": "^4.0.0" + }, + "peerDependencies": { + "react": "^17 || ^18", + "react-dom": "^17 || ^18" + } + }, + "node_modules/@patternfly/react-code-editor": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@patternfly/react-code-editor/-/react-code-editor-6.0.0.tgz", + "integrity": "sha512-TnI/NNkizzWTzdVZWmpyEPKXgsOoUeklk8Xlgtl7II/+5juLjlt0wXTMhL35F59Rzd0YohGs251zXAwJbn6vIQ==", + "dependencies": { + "@monaco-editor/react": "^4.6.0", + "@patternfly/react-core": "^6.0.0", + "@patternfly/react-icons": "^6.0.0", + "@patternfly/react-styles": "^6.0.0", + "react-dropzone": "14.2.3", + "tslib": "^2.7.0" + }, + "peerDependencies": { + "react": "^17 || ^18", + "react-dom": "^17 || ^18" + } + }, "node_modules/@patternfly/react-core": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-6.0.0.tgz", @@ -1260,6 +987,28 @@ "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-6.0.0.tgz", "integrity": "sha512-xd0ynDkiIW2rp8jz4TNvR4Dyaw9kSMkZdsuYcLlFXCVmvX//Mnl4rhBnid/2j2TaqK0NbkyTTPnPY/BU7SfLVQ==" }, + "node_modules/@patternfly/virtual-assistant": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@patternfly/virtual-assistant/-/virtual-assistant-2.0.2.tgz", + "integrity": "sha512-2qVQg1cU3Wv+mkFklAYQWyXWICbRlL/vgllSORGmBL0VIYM+Q3ICD3z/CkQrt0K1qdEqXp60dCnu1BKODrptxQ==", + "dependencies": { + "@patternfly/react-code-editor": "^6.0.0", + "@patternfly/react-core": "^6.0.0", + "@patternfly/react-icons": "^6.0.0", + "clsx": "^2.1.0", + "framer-motion": "^11.3.28", + "path-browserify": "^1.0.1", + "react-jss": "^10.10.0", + "react-markdown": "^9.0.1", + "react-syntax-highlighter": "^15.5.0", + "react-textarea-auto-witdth-height": "^1.0.3", + "remark-gfm": "^4.0.0" + }, + "peerDependencies": { + "react": "^17 || ^18", + "react-dom": "^17 || ^18" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -1376,6 +1125,35 @@ "tslib": "^2.4.0" } }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/hoist-non-react-statics": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz", @@ -1400,6 +1178,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==" + }, "node_modules/@types/node": { "version": "22.5.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.2.tgz", @@ -1413,14 +1204,12 @@ "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", - "dev": true, "license": "MIT" }, "node_modules/@types/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.1.tgz", "integrity": "sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==", - "dev": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -1452,6 +1241,17 @@ "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==" }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "optional": true + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" + }, "node_modules/@types/uuid": { "version": "10.0.0", "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-10.0.0.tgz", @@ -1681,7 +1481,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true, "license": "ISC" }, "node_modules/acorn": { @@ -2010,6 +1809,15 @@ "deep-equal": "^2.0.5" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -2031,7 +1839,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -2133,6 +1941,67 @@ } ] }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chokidar": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "optional": true, + "peer": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/clean-git-ref": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/clean-git-ref/-/clean-git-ref-2.0.1.tgz", @@ -2143,6 +2012,14 @@ "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, "node_modules/color": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", @@ -2200,6 +2077,15 @@ "node": ">= 0.8" } }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2248,11 +2134,29 @@ "node": ">= 8" } }, + "node_modules/css-jss": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/css-jss/-/css-jss-10.10.0.tgz", + "integrity": "sha512-YyMIS/LsSKEGXEaVJdjonWe18p4vXLo8CMA4FrW/kcaEyqdIGKCFXao31gbJddXEdIxSXFFURWrenBJPlKTgAA==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "^10.10.0", + "jss-preset-default": "^10.10.0" + } + }, + "node_modules/css-vendor": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/css-vendor/-/css-vendor-2.0.8.tgz", + "integrity": "sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==", + "dependencies": { + "@babel/runtime": "^7.8.3", + "is-in-browser": "^1.0.2" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "dev": true, "license": "MIT" }, "node_modules/damerau-levenshtein": { @@ -2325,11 +2229,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/date-fns": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", + "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/debug": { "version": "4.3.5", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.5.tgz", "integrity": "sha512-pt0bNEmneDIvdL1Xsd9oDQ/wrQRkXDT4AUWlNZNPKvW5x/jyO9VFXkJUP07vQ2upmw5PlaITaPKc31jK13V+jg==", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.1.2" @@ -2343,6 +2255,18 @@ } } }, + "node_modules/decode-named-character-reference": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", + "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/decompress-response": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", @@ -2442,6 +2366,14 @@ "node": ">=0.4.0" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", @@ -2450,6 +2382,18 @@ "node": ">=8" } }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/diff3": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/diff3/-/diff3-0.0.3.tgz", @@ -2481,6 +2425,14 @@ "node": ">=6.0.0" } }, + "node_modules/dompurify": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.2.tgz", + "integrity": "sha512-YMM+erhdZ2nkZ4fTNRTSI94mb7VG7uVF5vj5Zde7tImgnhZE3R6YW/IACGIHb2ux+QkEXMhe591N+5jWOmL4Zw==", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2719,12 +2671,23 @@ "node": ">=6" } }, - "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", - "dev": true, - "license": "MIT", + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -3577,6 +3540,15 @@ "node": ">=4.0" } }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -3587,6 +3559,11 @@ "node": ">=0.10.0" } }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3655,6 +3632,18 @@ "reusify": "^1.0.4" } }, + "node_modules/fault": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fault/-/fault-1.0.4.tgz", + "integrity": "sha512-CJ0HCB5tL5fYTEA7ToAq5+kTwd++Borf1/bifxd9iT70QcXr4MRrO3Llf8Ifs70q+SJcGHFtnIE/Nw6giCtECA==", + "dependencies": { + "format": "^0.2.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/fecha": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", @@ -3712,7 +3701,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -3835,6 +3824,14 @@ "node": ">= 6" } }, + "node_modules/format": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", + "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", + "engines": { + "node": ">=0.4.x" + } + }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -3847,6 +3844,32 @@ "node": ">=12.20.0" } }, + "node_modules/framer-motion": { + "version": "11.13.1", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.13.1.tgz", + "integrity": "sha512-F40tpGTHByhn9h3zdBQPcEro+pSLtzARcocbNqAyfBI+u9S+KZuHH/7O9+z+GEkoF3eqFxfvVw0eBDytohwqmQ==", + "dependencies": { + "motion-dom": "^11.13.0", + "motion-utils": "^11.13.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fs": { "version": "0.0.1-security", "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", @@ -4162,16 +4185,148 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-parse-selector": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-2.2.5.tgz", + "integrity": "sha512-7j6mrk/qqkSehsM92wQjdIgWM2/BW61u/53G6xmC8i1OmEdKLHbk419QKQUjz6LglWsfqoiHmyMRkP1BGjecNQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.2.tgz", + "integrity": "sha512-1ngXYb+V9UT5h+PxNRa1O1FYguZK/XL+gkeqvp7EdHlB9oHUG0eYRo/vY5inBdcqo3RkPMC58/H94HvkbfGdyg==", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-object": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-6.0.0.tgz", + "integrity": "sha512-nDM6bvd7lIqDUiYEiu5Sl/+6ReP0BMk/2f4U/Rooccxkj0P5nm+acM5PrGJ/t5I8qPGiqZSE6hVAwZEdZIvP4w==", + "dependencies": { + "@types/hast": "^2.0.0", + "comma-separated-tokens": "^1.0.0", + "hast-util-parse-selector": "^2.0.0", + "property-information": "^5.0.0", + "space-separated-tokens": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript/node_modules/@types/hast": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-2.3.10.tgz", + "integrity": "sha512-McWspRw8xx8J9HurkVBfYj0xKoE25tOFlHGdx4MJ5xORQrMGZNqJhVQWaIbm6Oyla5kYOXtDiopzKRJzEOkwJw==", + "dependencies": { + "@types/unist": "^2" + } + }, + "node_modules/hastscript/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" + }, + "node_modules/hastscript/node_modules/comma-separated-tokens": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-1.0.8.tgz", + "integrity": "sha512-GHuDRO12Sypu2cV70d1dkA2EUmXHgntrzbpvOB+Qy+49ypNfGgFQIC2fhhXbnyrJRynDCAARsT7Ou0M6hirpfw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hastscript/node_modules/property-information": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-5.6.0.tgz", + "integrity": "sha512-YUHSPk+A30YPv+0Qf8i9Mbfe/C0hdPXk1s1jPVToV8pk8BQtpw10ct89Eo7OWkutrwqvT0eicAxlOg3dOAu8JA==", + "dependencies": { + "xtend": "^4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/hastscript/node_modules/space-separated-tokens": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-1.1.5.tgz", + "integrity": "sha512-q/JSVd1Lptzhf5bkYm4ob4iWPjx0KiRe3sRFBNrVqbJkFaBm5vbbowy1mymoPNLRa52+oadOhJ+K49wsSeSjTA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "engines": { + "node": "*" + } + }, + "node_modules/highlightjs-vue": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/highlightjs-vue/-/highlightjs-vue-1.0.0.tgz", + "integrity": "sha512-PDEfEF102G23vHmPhLyPboFCD+BkMGu+GuJe2d9/eH4FsCwvgBpnc9n0pGE+ffKdph38s6foEZiEjdgHdzp+IA==" + }, "node_modules/hoist-non-react-statics": { "version": "3.3.2", "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "react-is": "^16.7.0" } }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hyphenate-style-name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.1.0.tgz", + "integrity": "sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==" + }, "node_modules/ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", @@ -4181,6 +4336,13 @@ "node": ">= 4" } }, + "node_modules/immutable": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz", + "integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==", + "optional": true, + "peer": true + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -4226,6 +4388,11 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/inline-style-parser": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", + "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==" + }, "node_modules/internal-slot": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", @@ -4241,6 +4408,28 @@ "node": ">= 0.4" } }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-arguments": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", @@ -4388,11 +4577,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -4441,7 +4639,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -4450,6 +4648,20 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-in-browser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/is-in-browser/-/is-in-browser-1.1.3.tgz", + "integrity": "sha512-FeXIBgG/CPGd/WUxuEyvgGTEfwiG9Z4EKGxjNMRqviiIIfsmgrpnHLffEDdwUHqNva1VEW91o3xBT/m8Elgl9g==" + }, "node_modules/is-map": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", @@ -4480,7 +4692,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -4512,6 +4724,17 @@ "node": ">=8" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -4679,7 +4902,6 @@ "version": "1.27.2", "resolved": "https://registry.npmjs.org/isomorphic-git/-/isomorphic-git-1.27.2.tgz", "integrity": "sha512-nCiz+ieOkWb5kDJSSckDTiMjTcgkxqH2xuiQmw1Y6O/spwx4d6TKYSfGCd4f71HGvUYcRSUGqJEI+3uN6UQlOw==", - "license": "MIT", "dependencies": { "async-lock": "^1.4.1", "clean-git-ref": "^2.0.1", @@ -4808,6 +5030,158 @@ "node": ">=6" } }, + "node_modules/jss": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss/-/jss-10.10.0.tgz", + "integrity": "sha512-cqsOTS7jqPsPMjtKYDUpdFC0AbhYFLTcuGRqymgmdJIeQ8cH7+AgX7YSgQy79wXloZq2VvATYxUOUQEvS1V/Zw==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "csstype": "^3.0.2", + "is-in-browser": "^1.1.3", + "tiny-warning": "^1.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/jss" + } + }, + "node_modules/jss-plugin-camel-case": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-camel-case/-/jss-plugin-camel-case-10.10.0.tgz", + "integrity": "sha512-z+HETfj5IYgFxh1wJnUAU8jByI48ED+v0fuTuhKrPR+pRBYS2EDwbusU8aFOpCdYhtRc9zhN+PJ7iNE8pAWyPw==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "hyphenate-style-name": "^1.0.3", + "jss": "10.10.0" + } + }, + "node_modules/jss-plugin-compose": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-compose/-/jss-plugin-compose-10.10.0.tgz", + "integrity": "sha512-F5kgtWpI2XfZ3Z8eP78tZEYFdgTIbpA/TMuX3a8vwrNolYtN1N4qJR/Ob0LAsqIwCMLojtxN7c7Oo/+Vz6THow==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0", + "tiny-warning": "^1.0.2" + } + }, + "node_modules/jss-plugin-default-unit": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-default-unit/-/jss-plugin-default-unit-10.10.0.tgz", + "integrity": "sha512-SvpajxIECi4JDUbGLefvNckmI+c2VWmP43qnEy/0eiwzRUsafg5DVSIWSzZe4d2vFX1u9nRDP46WCFV/PXVBGQ==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0" + } + }, + "node_modules/jss-plugin-expand": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-expand/-/jss-plugin-expand-10.10.0.tgz", + "integrity": "sha512-ymT62W2OyDxBxr7A6JR87vVX9vTq2ep5jZLIdUSusfBIEENLdkkc0lL/Xaq8W9s3opUq7R0sZQpzRWELrfVYzA==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0" + } + }, + "node_modules/jss-plugin-extend": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-extend/-/jss-plugin-extend-10.10.0.tgz", + "integrity": "sha512-sKYrcMfr4xxigmIwqTjxNcHwXJIfvhvjTNxF+Tbc1NmNdyspGW47Ey6sGH8BcQ4FFQhLXctpWCQSpDwdNmXSwg==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0", + "tiny-warning": "^1.0.2" + } + }, + "node_modules/jss-plugin-global": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-global/-/jss-plugin-global-10.10.0.tgz", + "integrity": "sha512-icXEYbMufiNuWfuazLeN+BNJO16Ge88OcXU5ZDC2vLqElmMybA31Wi7lZ3lf+vgufRocvPj8443irhYRgWxP+A==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0" + } + }, + "node_modules/jss-plugin-nested": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-nested/-/jss-plugin-nested-10.10.0.tgz", + "integrity": "sha512-9R4JHxxGgiZhurDo3q7LdIiDEgtA1bTGzAbhSPyIOWb7ZubrjQe8acwhEQ6OEKydzpl8XHMtTnEwHXCARLYqYA==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0", + "tiny-warning": "^1.0.2" + } + }, + "node_modules/jss-plugin-props-sort": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-props-sort/-/jss-plugin-props-sort-10.10.0.tgz", + "integrity": "sha512-5VNJvQJbnq/vRfje6uZLe/FyaOpzP/IH1LP+0fr88QamVrGJa0hpRRyAa0ea4U/3LcorJfBFVyC4yN2QC73lJg==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0" + } + }, + "node_modules/jss-plugin-rule-value-function": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.10.0.tgz", + "integrity": "sha512-uEFJFgaCtkXeIPgki8ICw3Y7VMkL9GEan6SqmT9tqpwM+/t+hxfMUdU4wQ0MtOiMNWhwnckBV0IebrKcZM9C0g==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0", + "tiny-warning": "^1.0.2" + } + }, + "node_modules/jss-plugin-rule-value-observable": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-rule-value-observable/-/jss-plugin-rule-value-observable-10.10.0.tgz", + "integrity": "sha512-ZLMaYrR3QE+vD7nl3oNXuj79VZl9Kp8/u6A1IbTPDcuOu8b56cFdWRZNZ0vNr8jHewooEeq2doy8Oxtymr2ZPA==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0", + "symbol-observable": "^1.2.0" + } + }, + "node_modules/jss-plugin-template": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-template/-/jss-plugin-template-10.10.0.tgz", + "integrity": "sha512-ocXZBIOJOA+jISPdsgkTs8wwpK6UbsvtZK5JI7VUggTD6LWKbtoxUzadd2TpfF+lEtlhUmMsCkTRNkITdPKa6w==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0", + "tiny-warning": "^1.0.2" + } + }, + "node_modules/jss-plugin-vendor-prefixer": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.10.0.tgz", + "integrity": "sha512-UY/41WumgjW8r1qMCO8l1ARg7NHnfRVWRhZ2E2m0DMYsr2DD91qIXLyNhiX83hHswR7Wm4D+oDYNC1zWCJWtqg==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "css-vendor": "^2.0.8", + "jss": "10.10.0" + } + }, + "node_modules/jss-preset-default": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/jss-preset-default/-/jss-preset-default-10.10.0.tgz", + "integrity": "sha512-GL175Wt2FGhjE+f+Y3aWh+JioL06/QWFgZp53CbNNq6ZkVU0TDplD8Bxm9KnkotAYn3FlplNqoW5CjyLXcoJ7Q==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "jss": "10.10.0", + "jss-plugin-camel-case": "10.10.0", + "jss-plugin-compose": "10.10.0", + "jss-plugin-default-unit": "10.10.0", + "jss-plugin-expand": "10.10.0", + "jss-plugin-extend": "10.10.0", + "jss-plugin-global": "10.10.0", + "jss-plugin-nested": "10.10.0", + "jss-plugin-props-sort": "10.10.0", + "jss-plugin-rule-value-function": "10.10.0", + "jss-plugin-rule-value-observable": "10.10.0", + "jss-plugin-template": "10.10.0", + "jss-plugin-vendor-prefixer": "10.10.0" + } + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", @@ -4919,6 +5293,15 @@ "node": ">= 12.0.0" } }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -4931,6 +5314,19 @@ "loose-envify": "cli.js" } }, + "node_modules/lowlight": { + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-1.20.0.tgz", + "integrity": "sha512-8Ktj+prEb1RoCPkEOrPMYUN/nCggB7qAWe3a7OpMjWQkh3l2RD5wKRQ+o8Q8YuI9RG/xs95waaI/E6ym/7NsTw==", + "dependencies": { + "fault": "^1.0.0", + "highlight.js": "~10.7.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -4941,21 +5337,820 @@ "yallist": "^3.0.2" } }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 8" + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", - "dev": true, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz", + "integrity": "sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", + "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.0.0.tgz", + "integrity": "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.0.0.tgz", + "integrity": "sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.1.3.tgz", + "integrity": "sha512-bfOjvNt+1AcbPLTFMFWY149nJz0OjmewJs3LQQ5pIyVGxP4CdOqNVJL6kTaM5c68p8q82Xv3nCyFfUnuEcH3UQ==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromark": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.1.tgz", + "integrity": "sha512-eBPdkcoCNvYcxQOAKAlceo5SNdzZWfF+FcSupREAzdAh9rRmE239CEQAiTwIgblwnoM8zzj35sZ5ZwvSEOF6Kw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.2.tgz", + "integrity": "sha512-FKjQKbxd1cibWMM1P9N+H8TwlgGgSkWZMmfuVucLCHaYqeSvJ0hFeHsIa65pA2nYbes0f8LDHPMrd9X7Ujxg9w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.0.tgz", + "integrity": "sha512-Ub2ncQv+fwD70/l4ou27b4YzfNaCJOvyX4HxXU15m7mpYY+rjuWzsLIPZHJL253Z643RpbcP1oeIJlQ/SKW67g==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.3.tgz", + "integrity": "sha512-VXJJuNxYWSoYL6AJ6OQECCFGhIU2GGHMw8tahogePBrjkG8aCCas3ibkp7RnVOSTClg2is05/R7maAhF1XyQMg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromark-util-types": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.1.tgz", + "integrity": "sha512-534m2WhVTddrcKVepwmVEVnUAmtrx9bfIjNoQHRqfnvdaHQiFytEhJoTgpWJvDEXCO5gLTQh3wYC1PgOJA4NSQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ] + }, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "devOptional": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -5040,6 +6235,22 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/monaco-editor": { + "version": "0.52.0", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.52.0.tgz", + "integrity": "sha512-OeWhNpABLCeTqubfqLMXGsqf6OmPU6pHM85kF3dhy6kq5hnhuVS1p3VrEW/XhWHc71P2tHyS5JFySD8mgs1crw==", + "peer": true + }, + "node_modules/motion-dom": { + "version": "11.13.0", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-11.13.0.tgz", + "integrity": "sha512-Oc1MLGJQ6nrvXccXA89lXtOqFyBmvHtaDcTRGT66o8Czl7nuA8BeHAd9MQV1pQKX0d2RHFBFaw5g3k23hQJt0w==" + }, + "node_modules/motion-utils": { + "version": "11.13.0", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-11.13.0.tgz", + "integrity": "sha512-lq6TzXkH5c/ysJQBxgLXgM01qwBH1b4goTPh57VvZWJbVJZF/0SB31UWEn4EIqbVPf3au88n2rvK17SpDTja1A==" + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -5192,6 +6403,13 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "optional": true, + "peer": true + }, "node_modules/node-domexception": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", @@ -5532,11 +6750,34 @@ "node": ">=6" } }, + "node_modules/parse-entities": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.1.tgz", + "integrity": "sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==" + }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", - "license": "MIT" + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" }, "node_modules/path-exists": { "version": "4.0.0", @@ -5618,7 +6859,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -5741,6 +6982,14 @@ "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==", "license": "MIT" }, + "node_modules/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "engines": { + "node": ">=6" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5752,6 +7001,15 @@ "react-is": "^16.13.1" } }, + "node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -5801,6 +7059,11 @@ "node": ">=0.10.0" } }, + "node_modules/react-display-name": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/react-display-name/-/react-display-name-0.2.5.tgz", + "integrity": "sha512-I+vcaK9t4+kypiSgaiVWAipqHRXYmZIuAiS8vzFvXHHXVigg/sMKwlRgLy6LH2i3rmP+0Vzfl5lFsFRwF1r3pg==" + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", @@ -5837,6 +7100,76 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/react-jss": { + "version": "10.10.0", + "resolved": "https://registry.npmjs.org/react-jss/-/react-jss-10.10.0.tgz", + "integrity": "sha512-WLiq84UYWqNBF6579/uprcIUnM1TSywYq6AIjKTTTG5ziJl9Uy+pwuvpN3apuyVwflMbD60PraeTKT7uWH9XEQ==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "@emotion/is-prop-valid": "^0.7.3", + "css-jss": "10.10.0", + "hoist-non-react-statics": "^3.2.0", + "is-in-browser": "^1.1.3", + "jss": "10.10.0", + "jss-preset-default": "10.10.0", + "prop-types": "^15.6.0", + "shallow-equal": "^1.2.0", + "theming": "^3.3.0", + "tiny-warning": "^1.0.2" + }, + "peerDependencies": { + "react": ">=16.8.6" + } + }, + "node_modules/react-markdown": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.0.1.tgz", + "integrity": "sha512-186Gw/vF1uRkydbsOIkcGXw7aHq0sZOCRFFjGrr7b9+nVZg4UfA4enXCaxm4fUzecU38sWfrNDitGhshuU7rdg==", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, + "node_modules/react-syntax-highlighter": { + "version": "15.6.1", + "resolved": "https://registry.npmjs.org/react-syntax-highlighter/-/react-syntax-highlighter-15.6.1.tgz", + "integrity": "sha512-OqJ2/vL7lEeV5zTJyG7kmARppUjiB9h9udl4qHQjjgEos66z00Ia0OckwYfRxCSFrW8RJIBnsBwQsHZbVPspqg==", + "dependencies": { + "@babel/runtime": "^7.3.1", + "highlight.js": "^10.4.1", + "highlightjs-vue": "^1.0.0", + "lowlight": "^1.17.0", + "prismjs": "^1.27.0", + "refractor": "^3.6.0" + }, + "peerDependencies": { + "react": ">= 0.14.0" + } + }, + "node_modules/react-textarea-auto-witdth-height": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/react-textarea-auto-witdth-height/-/react-textarea-auto-witdth-height-1.0.3.tgz", + "integrity": "sha512-12NbXe+OBmwv1VCMdOKmjrHwLHrsVbOHn2Wm/xER6jtvARj0bX73skhJnywhhbuQ2FfX9Y57AkpnkQTJbwwBxA==", + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -5851,6 +7184,20 @@ "node": ">= 6" } }, + "node_modules/readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "optional": true, + "peer": true, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", @@ -5873,6 +7220,112 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/refractor": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/refractor/-/refractor-3.6.0.tgz", + "integrity": "sha512-MY9W41IOWxxk31o+YvFCNyNzdkc9M20NoZK5vq6jkv4I/uh2zkWcfudj0Q1fovjUQJrNewS9NMzeTtqPf+n5EA==", + "dependencies": { + "hastscript": "^6.0.0", + "parse-entities": "^2.0.0", + "prismjs": "~1.27.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-entities": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", + "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-entities-legacy": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", + "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/character-reference-invalid": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", + "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-alphabetical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", + "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-alphanumerical": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", + "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "dependencies": { + "is-alphabetical": "^1.0.0", + "is-decimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-decimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", + "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/is-hexadecimal": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", + "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/parse-entities": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", + "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", + "dependencies": { + "character-entities": "^1.0.0", + "character-entities-legacy": "^1.0.0", + "character-reference-invalid": "^1.0.0", + "is-alphanumerical": "^1.0.0", + "is-decimal": "^1.0.0", + "is-hexadecimal": "^1.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/refractor/node_modules/prismjs": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.27.0.tgz", + "integrity": "sha512-t13BGPUlFDR7wRB5kQDG4jjl7XeuH6jbJGt11JHPL96qwsEHNX2+68tFXqc1/k+/jALsbSWJKUOT/hcYAZ5LkA==", + "engines": { + "node": ">=6" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", @@ -5898,6 +7351,68 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/remark-gfm": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.0.tgz", + "integrity": "sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.1.tgz", + "integrity": "sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ==", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -6099,6 +7614,27 @@ "node": ">=10" } }, + "node_modules/sass": { + "version": "1.82.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.82.0.tgz", + "integrity": "sha512-j4GMCTa8elGyN9A7x7bEglx0VgSpNUG4W4wNedQ33wSMdnkqQCT8HTwOaVSV4e6yQovcu/3Oc4coJP/l0xhL2Q==", + "optional": true, + "peer": true, + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -6164,6 +7700,11 @@ "sha.js": "bin.js" } }, + "node_modules/shallow-equal": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz", + "integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==" + }, "node_modules/sharp": { "version": "0.33.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", @@ -6367,6 +7908,15 @@ "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -6376,6 +7926,11 @@ "node": "*" } }, + "node_modules/state-local": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/state-local/-/state-local-1.0.7.tgz", + "integrity": "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==" + }, "node_modules/stop-iteration-iterator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", @@ -6566,6 +8121,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -6616,6 +8184,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-to-object": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.8.tgz", + "integrity": "sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==", + "dependencies": { + "inline-style-parser": "0.2.4" + } + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -6651,6 +8227,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/synckit": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", @@ -6696,11 +8280,33 @@ "dev": true, "license": "MIT" }, + "node_modules/theming": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/theming/-/theming-3.3.0.tgz", + "integrity": "sha512-u6l4qTJRDaWZsqa8JugaNt7Xd8PPl9+gonZaIe28vAhqgHMIG/DOyFPqiKN/gQLQYj05tHv+YQdNILL4zoiAVA==", + "dependencies": { + "hoist-non-react-statics": "^3.3.0", + "prop-types": "^15.5.8", + "react-display-name": "^0.2.4", + "tiny-warning": "^1.0.2" + }, + "engines": { + "node": ">=8" + }, + "peerDependencies": { + "react": ">=16.3" + } + }, + "node_modules/tiny-warning": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", + "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -6709,6 +8315,15 @@ "node": ">=8.0" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/triple-beam": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", @@ -6717,6 +8332,15 @@ "node": ">= 14.0.0" } }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", @@ -6899,6 +8523,87 @@ "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", "dev": true }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", @@ -6958,6 +8663,32 @@ "uuid": "dist/esm/bin/uuid" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/web-streams-polyfill": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", @@ -7253,6 +8984,14 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/yallist": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", @@ -7272,6 +9011,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/package.json b/package.json index 66263049..f12e0212 100644 --- a/package.json +++ b/package.json @@ -19,11 +19,15 @@ "dependencies": { "@fortawesome/fontawesome-svg-core": "^6.7.1", "@next/env": "^15.0.3", + "@patternfly/chatbot": "^2.1.0-prerelease.17", "@patternfly/react-core": "^6.0.0", "@patternfly/react-icons": "^6.0.0", "@patternfly/react-styles": "^6.0.0", "@patternfly/react-table": "^6.0.0", "axios": "^1.7.9", + "@patternfly/virtual-assistant": "^2.0.2", + "date-fns": "^4.1.0", + "dompurify": "^3.2.2", "fs": "^0.0.1-security", "isomorphic-git": "^1.27.2", "js-yaml": "^4.1.0", diff --git a/src/app/api/fine-tune/data-sets/route.ts b/src/app/api/fine-tune/data-sets/route.ts new file mode 100644 index 00000000..e239d6ae --- /dev/null +++ b/src/app/api/fine-tune/data-sets/route.ts @@ -0,0 +1,22 @@ +// src/pages/api/fine-tune/data-sets.ts +'use server'; + +import { NextRequest, NextResponse } from 'next/server'; + +export async function GET(req: NextRequest) { + try { + const API_SERVER = process.env.NEXT_PUBLIC_API_SERVER!; + + const response = await fetch(`${API_SERVER}/data`); + const data = await response.json(); + + if (!response.ok) { + return NextResponse.json({ error: 'Failed to fetch datasets' }, { status: response.status }); + } + + return NextResponse.json(data, { status: 200 }); + } catch (error) { + console.error('Error fetching datasets:', error); + return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); + } +} diff --git a/src/app/api/fine-tune/data/generate/route.ts b/src/app/api/fine-tune/data/generate/route.ts new file mode 100644 index 00000000..fb2e802f --- /dev/null +++ b/src/app/api/fine-tune/data/generate/route.ts @@ -0,0 +1,26 @@ +'use server'; + +import { NextResponse } from 'next/server'; + +export async function POST(request: Request) { + try { + const API_SERVER = process.env.NEXT_PUBLIC_API_SERVER!; + + const response = await fetch(`${API_SERVER}/data/generate`, { + method: 'POST' + }); + + if (!response.ok) { + console.error('Error response from API server:', response.status, response.statusText); + return NextResponse.json({ error: 'Failed to generate data' }, { status: response.status }); + } + + const responseData = await response.json(); + + // Return the response from the API server to the client + return NextResponse.json(responseData, { status: 200 }); + } catch (error) { + console.error('Error generating data:', error); + return NextResponse.json({ error: 'An error occurred while generating data' }, { status: 500 }); + } +} diff --git a/src/app/api/fine-tune/jobs/[job_id]/logs/route.ts b/src/app/api/fine-tune/jobs/[job_id]/logs/route.ts new file mode 100644 index 00000000..9017adb8 --- /dev/null +++ b/src/app/api/fine-tune/jobs/[job_id]/logs/route.ts @@ -0,0 +1,29 @@ +// src/app/api/fine-tune/jobs/[job_id]/logs/route.ts +import { NextResponse } from 'next/server'; + +const API_SERVER = process.env.NEXT_PUBLIC_API_SERVER!; + +export async function GET(request: Request, { params }: { params: { job_id: string } }) { + const { job_id } = await Promise.resolve(params); + + try { + const response = await fetch(`${API_SERVER}/jobs/${job_id}/logs`, { + method: 'GET' + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('Error from API server:', errorText); + return NextResponse.json({ error: 'Error fetching logs' }, { status: 500 }); + } + + const logs = await response.text(); + return new NextResponse(logs, { + status: 200, + headers: { 'Content-Type': 'text/plain' } + }); + } catch (error) { + console.error('Error fetching logs:', error); + return NextResponse.json({ error: 'Error fetching logs' }, { status: 500 }); + } +} diff --git a/src/app/api/fine-tune/jobs/[job_id]/status/route.ts b/src/app/api/fine-tune/jobs/[job_id]/status/route.ts new file mode 100644 index 00000000..8b793a6b --- /dev/null +++ b/src/app/api/fine-tune/jobs/[job_id]/status/route.ts @@ -0,0 +1,29 @@ +// src/app/api/fine-tune/jobs/[job_id]/status/route.ts +'use server'; + +import { NextResponse } from 'next/server'; + +export async function GET(request: Request, { params }: { params: { job_id: string } }) { + const { job_id } = params; + const API_SERVER = process.env.NEXT_PUBLIC_API_SERVER!; + + try { + // Forward the request to the API server + const response = await fetch(`${API_SERVER}/jobs/${job_id}/status`, { + method: 'GET' + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error('Error from API server:', errorText); + return NextResponse.json({ error: 'Error fetching job status' }, { status: 500 }); + } + + const result = await response.json(); + // Return the job status to the client + return NextResponse.json(result, { status: 200 }); + } catch (error) { + console.error('Error fetching job status:', error); + return NextResponse.json({ error: 'Error fetching job status' }, { status: 500 }); + } +} diff --git a/src/app/api/fine-tune/jobs/route.ts b/src/app/api/fine-tune/jobs/route.ts new file mode 100644 index 00000000..ef5dead3 --- /dev/null +++ b/src/app/api/fine-tune/jobs/route.ts @@ -0,0 +1,22 @@ +// src/app/api/fine-tune/jobs/route.ts +'use server'; + +import { NextResponse } from 'next/server'; + +export async function GET(request: Request) { + const API_SERVER = process.env.NEXT_PUBLIC_API_SERVER!; + + try { + const response = await fetch(`${API_SERVER}/jobs`); + if (!response.ok) { + const errorText = await response.text(); + console.error('Error from API server:', errorText); + return NextResponse.json({ error: 'Error fetching jobs' }, { status: 500 }); + } + const result = await response.json(); + return NextResponse.json(result, { status: 200 }); + } catch (error) { + console.error('Error fetching jobs:', error); + return NextResponse.json({ error: 'Error fetching jobs' }, { status: 500 }); + } +} diff --git a/src/app/api/fine-tune/model/serve-base/route.ts b/src/app/api/fine-tune/model/serve-base/route.ts new file mode 100644 index 00000000..aa415db3 --- /dev/null +++ b/src/app/api/fine-tune/model/serve-base/route.ts @@ -0,0 +1,54 @@ +// src/app/api/model/serve-base/route.ts +'use server'; + +import { NextResponse } from 'next/server'; + +export async function POST() { + try { + console.log('Received serve-base model request'); + + const API_SERVER = process.env.NEXT_PUBLIC_API_SERVER!; + const endpoint = `${API_SERVER}/model/serve-base`; + + console.log(`Forwarding request to the API server: ${endpoint}`); + + // No request body needed for serving the base model + const response = await fetch(endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }); + + console.log('Response from API server (serve-base):', { + status: response.status, + statusText: response.statusText + }); + + if (!response.ok) { + console.error('Error response from the API server:', response.status, response.statusText); + return NextResponse.json({ error: 'Failed to serve the base model on the API server' }, { status: response.status }); + } + + // Parse response safely + let responseData; + try { + const text = await response.text(); + responseData = text ? JSON.parse(text) : {}; + console.log('Parsed response data (serve-base):', responseData); + } catch (error) { + console.error('Error parsing JSON response from API server:', error); + return NextResponse.json({ error: 'Invalid JSON response from the API server' }, { status: 500 }); + } + + if (!responseData.job_id) { + console.error('Missing job_id in API server response for serve-base:', responseData); + return NextResponse.json({ error: 'API server response does not contain job_id' }, { status: 500 }); + } + + // Return the response from the API server to the client + console.log('Returning success response with job_id (serve-base):', responseData.job_id); + return NextResponse.json(responseData, { status: 200 }); + } catch (error) { + console.error('Unexpected error during serve-base:', error); + return NextResponse.json({ error: 'An unexpected error occurred during serving the base model' }, { status: 500 }); + } +} diff --git a/src/app/api/fine-tune/model/serve-latest/route.ts b/src/app/api/fine-tune/model/serve-latest/route.ts new file mode 100644 index 00000000..92b75593 --- /dev/null +++ b/src/app/api/fine-tune/model/serve-latest/route.ts @@ -0,0 +1,54 @@ +// src/app/api/model/serve-latest/route.ts +'use server'; + +import { NextResponse } from 'next/server'; + +export async function POST() { + try { + console.log('Received serve-latest model request'); + + const API_SERVER = process.env.NEXT_PUBLIC_API_SERVER!; + const endpoint = `${API_SERVER}/model/serve-latest`; + + console.log(`Forwarding request to API server: ${endpoint}`); + + // No request body needed for serving the latest model + const response = await fetch(endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' } + }); + + console.log('Response from API server:', { + status: response.status, + statusText: response.statusText + }); + + if (!response.ok) { + console.error('Error response from API server:', response.status, response.statusText); + return NextResponse.json({ error: 'Failed to serve the latest model on the API server' }, { status: response.status }); + } + + // Parse response safely + let responseData; + try { + const text = await response.text(); + responseData = text ? JSON.parse(text) : {}; + console.log('Parsed response data (serve-latest):', responseData); + } catch (error) { + console.error('Error parsing JSON response from API server:', error); + return NextResponse.json({ error: 'Invalid JSON response from the API server' }, { status: 500 }); + } + + if (!responseData.job_id) { + console.error('Missing job_id in API server response for serve-latest:', responseData); + return NextResponse.json({ error: 'API server response does not contain job_id' }, { status: 500 }); + } + + // Return the response from the API server to the client + console.log('Returning success response with job_id (serve-latest):', responseData.job_id); + return NextResponse.json(responseData, { status: 200 }); + } catch (error) { + console.error('Unexpected error during serve-latest:', error); + return NextResponse.json({ error: 'An unexpected error occurred during serving the latest model' }, { status: 500 }); + } +} diff --git a/src/app/api/fine-tune/model/train/route.ts b/src/app/api/fine-tune/model/train/route.ts new file mode 100644 index 00000000..aba8809d --- /dev/null +++ b/src/app/api/fine-tune/model/train/route.ts @@ -0,0 +1,70 @@ +// src/app/api/fine-tune/model/train +'use server'; + +import { NextResponse } from 'next/server'; + +export async function POST(request: Request) { + try { + console.log('Received train job request'); + + // Parse the request body for required data + const { modelName, branchName } = await request.json(); + const API_SERVER = process.env.NEXT_PUBLIC_API_SERVER!; + + console.log('Request body:', { modelName, branchName }); + + if (!modelName || !branchName) { + console.error('Missing required parameters: modelName and branchName'); + return NextResponse.json({ error: 'Missing required parameters: modelName and branchName' }, { status: 400 }); + } + + // Forward the request to the API server + const endpoint = `${API_SERVER}/model/train`; + + console.log(`Forwarding request to API server: ${API_SERVER}`); + + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + modelName, + branchName + }) + }); + + console.log('Response from API server:', { + status: response.status, + statusText: response.statusText + }); + + if (!response.ok) { + console.error('Error response from API server:', response.status, response.statusText); + return NextResponse.json({ error: 'Failed to train the model on the API server' }, { status: response.status }); + } + + // Parse response safely + let responseData; + try { + const text = await response.text(); + responseData = text ? JSON.parse(text) : {}; + console.log('Parsed response data:', responseData); + } catch (error) { + console.error('Error parsing JSON response from API server:', error); + return NextResponse.json({ error: 'Invalid JSON response from the API server' }, { status: 500 }); + } + + if (!responseData.job_id) { + console.error('Missing job_id in API server response:', responseData); + return NextResponse.json({ error: 'API server response does not contain job_id' }, { status: 500 }); + } + + // Return the response from the API server to the client + console.log('Returning success response with job_id:', responseData.job_id); + return NextResponse.json(responseData, { status: 200 }); + } catch (error) { + console.error('Unexpected error during training:', error); + return NextResponse.json({ error: 'An unexpected error occurred during training' }, { status: 500 }); + } +} diff --git a/src/app/api/fine-tune/models/route.ts b/src/app/api/fine-tune/models/route.ts new file mode 100644 index 00000000..7c0c3d1b --- /dev/null +++ b/src/app/api/fine-tune/models/route.ts @@ -0,0 +1,22 @@ +// src/pages/api/fine-tune/models/route.ts +'use server'; + +import { NextRequest, NextResponse } from 'next/server'; + +export async function GET(req: NextRequest) { + try { + const API_SERVER = process.env.NEXT_PUBLIC_API_SERVER!; + + const response = await fetch(`${API_SERVER}/models`); + const data = await response.json(); + + if (!response.ok) { + return NextResponse.json({ error: 'Failed to fetch models' }, { status: response.status }); + } + + return NextResponse.json(data, { status: 200 }); + } catch (error) { + console.error('Error fetching models:', error); + return NextResponse.json({ error: 'Internal Server Error' }, { status: 500 }); + } +} diff --git a/src/app/api/fine-tune/pipeline/generate-train/route.ts b/src/app/api/fine-tune/pipeline/generate-train/route.ts new file mode 100644 index 00000000..090dc490 --- /dev/null +++ b/src/app/api/fine-tune/pipeline/generate-train/route.ts @@ -0,0 +1,40 @@ +'use server'; + +import { NextResponse } from 'next/server'; + +export async function POST(request: Request) { + try { + // Parse the request body for required data + const { modelName, branchName } = await request.json(); + const API_SERVER = process.env.NEXT_PUBLIC_API_SERVER!; + + if (!modelName || !branchName) { + return NextResponse.json({ error: 'Missing required parameters: modelName and branchName' }, { status: 400 }); + } + + // Forward the request to the API server's pipeline endpoint + const endpoint = `${API_SERVER}/pipeline/generate-train`; + + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + modelName, + branchName + }) + }); + + if (!response.ok) { + console.error('Error response from API server (pipeline):', response.status, response.statusText); + return NextResponse.json({ error: 'Failed to run generate-train pipeline on the API server' }, { status: response.status }); + } + + const responseData = await response.json(); + return NextResponse.json(responseData, { status: 200 }); + } catch (error) { + console.error('Error during generate-train pipeline:', error); + return NextResponse.json({ error: 'An error occurred during generate-train pipeline' }, { status: 500 }); + } +} diff --git a/src/app/experimental/chat-eval/page.tsx b/src/app/experimental/chat-eval/page.tsx new file mode 100644 index 00000000..5f408703 --- /dev/null +++ b/src/app/experimental/chat-eval/page.tsx @@ -0,0 +1,17 @@ +// src/app/experimental/chat-eval/page.tsx +'use client'; + +import * as React from 'react'; +import '@patternfly/react-core/dist/styles/base.css'; +import { AppLayout } from '@/components/AppLayout'; +import ChatModelEval from '@/components/Experimental/ChatEval/ChatEval'; + +const ChatEval: React.FunctionComponent = () => { + return ( + + + + ); +}; + +export default ChatEval; diff --git a/src/app/experimental/fine-tune/page.tsx b/src/app/experimental/fine-tune/page.tsx new file mode 100644 index 00000000..82f74461 --- /dev/null +++ b/src/app/experimental/fine-tune/page.tsx @@ -0,0 +1,17 @@ +// src/app/experimental/fine-tune/page.tsx +'use client'; + +import * as React from 'react'; +import '@patternfly/react-core/dist/styles/base.css'; +import { AppLayout } from '@/components/AppLayout'; +import FineTuning from '@/components/Experimental/FineTuning'; + +const FineTune: React.FunctionComponent = () => { + return ( + + + + ); +}; + +export default FineTune; diff --git a/src/components/AppLayout.tsx b/src/components/AppLayout.tsx index 3023e6ce..fe059946 100644 --- a/src/components/AppLayout.tsx +++ b/src/components/AppLayout.tsx @@ -25,6 +25,7 @@ import UserMenu from './UserMenu/UserMenu'; import { useSession } from 'next-auth/react'; // import { useTheme } from '../context/ThemeContext'; import { useState } from 'react'; +import '@/components/app.scss'; interface IAppLayout { children: React.ReactNode; @@ -103,7 +104,9 @@ const AppLayout: React.FunctionComponent = ({ children }) => { { path: '/experimental/dashboard-local/', label: 'Local Dashboard' }, { path: '/experimental/contribute-local/skill/', label: 'Local Skill' }, { path: '/experimental/contribute-local/knowledge/', label: 'Local Knowledge' }, - { path: '/experimental/contribute-local/configuration-local/', label: 'Local Configuration' } + { path: '/experimental/contribute-local/configuration-local/', label: 'Local Configuration' }, + { path: '/experimental/fine-tune/', label: 'Fine-tuning' }, + { path: '/experimental/chat-eval/', label: 'Model Chat Eval' } ] } ].filter(Boolean) as Route[]; diff --git a/src/components/Experimental/ChatEval/ChatEval.tsx b/src/components/Experimental/ChatEval/ChatEval.tsx new file mode 100644 index 00000000..785550f4 --- /dev/null +++ b/src/components/Experimental/ChatEval/ChatEval.tsx @@ -0,0 +1,666 @@ +// src/components/Experimental/ChatEval/ChatEval.tsx +'use client'; + +import React, { useState, useEffect } from 'react'; +import { + Breadcrumb, + BreadcrumbItem, + PageBreadcrumb, + PageSection, + Content, + Title, + Button, + DropdownList, + DropdownItem, + ExpandableSection, + Spinner, + CodeBlock, + CodeBlockCode +} from '@patternfly/react-core'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faBroom } from '@fortawesome/free-solid-svg-icons'; +import { Endpoint, Model } from '@/types'; +import { + Chatbot, + ChatbotHeader, + ChatbotHeaderMain, + ChatbotHeaderActions, + ChatbotHeaderSelectorDropdown, + ChatbotContent, + MessageBox, + Message, + MessageProps, + MessageBar, + ChatbotWelcomePrompt, + ChatbotFooter, + ChatbotFootnote, + ChatbotAlert +} from '@patternfly/chatbot'; + +import logo from '../../../../public/bot-icon-chat-32x32.svg'; +import userLogo from '../../../../public/default-avatar.svg'; + +// TODO: get nextjs app router server side render working with the patternfly chatbot component. +const MODEL_SERVER_IP = 'http://x.x.x.x'; + +const ChatModelEval: React.FC = () => { + const [questionLeft, setQuestionLeft] = useState(''); + const [messagesLeft, setMessagesLeft] = useState([]); + const [selectedModelLeft, setSelectedModelLeft] = useState(null); + const [alertMessageLeft, setAlertMessageLeft] = useState<{ title: string; message: string; variant: 'danger' | 'info' } | undefined>(undefined); + const [isLoadingLeft, setIsLoadingLeft] = useState(false); + + const [questionRight, setQuestionRight] = useState(''); + const [messagesRight, setMessagesRight] = useState([]); + const [selectedModelRight, setSelectedModelRight] = useState(null); + const [alertMessageRight, setAlertMessageRight] = useState<{ title: string; message: string; variant: 'danger' | 'info' } | undefined>(undefined); + const [isLoadingRight, setIsLoadingRight] = useState(false); + + const systemRole = + 'You are a cautious assistant. You carefully follow instructions.' + + ' You are helpful and harmless and you follow ethical guidelines and promote positive behavior. Only answer questions on what you are trained on.'; + + const [customModels, setCustomModels] = useState([]); + const [defaultModels, setDefaultModels] = useState([]); + const allModels = [...defaultModels, ...customModels]; + + const [modelJobIdLeft, setModelJobIdLeft] = useState(undefined); + const [modelJobIdRight, setModelJobIdRight] = useState(undefined); + + const [showModelLoadingLeft, setShowModelLoadingLeft] = useState(false); + const [showModelLoadingRight, setShowModelLoadingRight] = useState(false); + + // For logs viewing + const [expandedJobsLeft, setExpandedJobsLeft] = useState>({}); + const [jobLogsLeft, setJobLogsLeft] = useState>({}); + + const [expandedJobsRight, setExpandedJobsRight] = useState>({}); + const [jobLogsRight, setJobLogsRight] = useState>({}); + + // Fetch models + useEffect(() => { + const fetchDefaultModels = async () => { + const response = await fetch('/api/envConfig'); + const envConfig = await response.json(); + + const defs: Model[] = [ + { name: 'Granite-7b', apiURL: envConfig.GRANITE_API, modelName: envConfig.GRANITE_MODEL_NAME }, + { name: 'Merlinite-7b', apiURL: envConfig.MERLINITE_API, modelName: envConfig.MERLINITE_MODEL_NAME } + ]; + + const storedEndpoints = localStorage.getItem('endpoints'); + const cust: Model[] = storedEndpoints + ? JSON.parse(storedEndpoints).map((endpoint: Endpoint) => ({ + name: endpoint.modelName, + apiURL: `${endpoint.url}`, + modelName: endpoint.modelName + })) + : []; + + setDefaultModels(defs); + setCustomModels(cust); + }; + + fetchDefaultModels(); + }, []); + + const handleServeModel = async (endpoint: string, side: 'left' | 'right') => { + // Show loading popup + if (side === 'left') { + setShowModelLoadingLeft(true); + setTimeout(() => setShowModelLoadingLeft(false), 3000); + } else { + setShowModelLoadingRight(true); + setTimeout(() => setShowModelLoadingRight(false), 3000); + } + + try { + const response = await fetch(endpoint, { + method: 'POST' + }); + + if (!response.ok) { + console.error(`Failed to serve model from endpoint ${endpoint}`); + return; + } + + const data = await response.json(); + const { job_id } = data; + if (!job_id) { + console.error('No job_id returned from serving model'); + return; + } + + const loadingAlert = { + title: 'Model Loading', + message: 'One moment while the model is loading. Click "View Logs" below for details.', + variant: 'info' as const + }; + + if (side === 'left') { + setModelJobIdLeft(job_id); + setAlertMessageLeft(loadingAlert); + setTimeout(() => setAlertMessageLeft(undefined), 3000); + } else { + setModelJobIdRight(job_id); + setAlertMessageRight(loadingAlert); + setTimeout(() => setAlertMessageRight(undefined), 3000); + } + + // Once the model is served, set the selected model to the served endpoint + // For base model: port 8000 + // For latest checkpoint: port 8001 + if (endpoint.includes('serve-base')) { + const servedModel: Model = { + name: 'Granite base model (Served)', + apiURL: `${MODEL_SERVER_IP}:8000`, // API endpoint for base model + modelName: 'granite-base-served' + }; + + if (side === 'left') { + setSelectedModelLeft(servedModel); + } else { + setSelectedModelRight(servedModel); + } + } else if (endpoint.includes('serve-latest')) { + const servedModel: Model = { + name: 'Granite fine tune checkpoint (Served)', + apiURL: `${MODEL_SERVER_IP}:8001`, // API endpoint for latest model + modelName: 'granite-latest-served' + }; + + if (side === 'left') { + setSelectedModelLeft(servedModel); + } else { + setSelectedModelRight(servedModel); + } + } + } catch (error) { + console.error('Error serving model:', error); + } + }; + + const onSelectModelLeft = async (_event: React.MouseEvent | undefined, value: string | number | undefined) => { + // If "Granite fine tune checkpoint" selected + if (value === 'Granite fine tune checkpoint') { + await handleServeModel('/api/fine-tune/model/serve-latest', 'left'); + return; + } + + // If "Granite base model" selected + if (value === 'Granite base model') { + await handleServeModel('/api/fine-tune/model/serve-base', 'left'); + return; + } + + const chosen = allModels.find((model) => model.name === value) || null; + setSelectedModelLeft(chosen); + setAlertMessageLeft(undefined); + }; + + const onSelectModelRight = async (_event: React.MouseEvent | undefined, value: string | number | undefined) => { + if (value === 'Granite fine tune checkpoint') { + await handleServeModel('/api/fine-tune/model/serve-latest', 'right'); + return; + } + + if (value === 'Granite base model') { + await handleServeModel('/api/fine-tune/model/serve-base', 'right'); + return; + } + + const chosen = allModels.find((model) => model.name === value) || null; + setSelectedModelRight(chosen); + setAlertMessageRight(undefined); + }; + + const handleCleanupLeft = () => { + setMessagesLeft([]); + setAlertMessageLeft(undefined); + }; + + const handleCleanupRight = () => { + setMessagesRight([]); + setAlertMessageRight(undefined); + }; + + // Common stream update handler + const handleStreamUpdate = (id: string, newContent: string, setMessagesFn: React.Dispatch>) => { + setMessagesFn((msgs) => { + const updated = [...msgs]; + const idx = updated.findIndex((m) => m.id === id); + if (idx !== -1) { + updated[idx].content = newContent; + } + return updated; + }); + }; + + const handleSend = async ( + side: 'left' | 'right', + message: string, + selectedModel: Model | null, + setSelectedModelFn: React.Dispatch>, + setQuestionFn: React.Dispatch>, + setIsLoadingFn: React.Dispatch>, + setAlertMessageFn: React.Dispatch>, + setMessagesFn: React.Dispatch> + ) => { + const trimmedMessage = message.trim(); + if (!trimmedMessage) { + return; + } + + if (!selectedModel) { + setTimeout(() => { + setAlertMessageFn({ + title: 'No Model Selected', + message: 'Please select a model before sending a prompt.', + variant: 'danger' + }); + }, 0); + return; + } + + setAlertMessageFn(undefined); + + const userMsgId = `${Date.now()}_user_${side}`; + setMessagesFn((msgs) => [ + ...msgs, + { + id: userMsgId, + role: 'user', + content: trimmedMessage, + name: 'User', + avatar: userLogo.src, + timestamp: new Date().toLocaleTimeString() + } + ]); + + setQuestionFn(''); + setIsLoadingFn(true); + + const messagesPayload = [ + { role: 'system', content: systemRole }, + { role: 'user', content: trimmedMessage } + ]; + + const requestData = { + model: selectedModel.modelName, + messages: messagesPayload, + stream: true + }; + + const botMessageId = `${Date.now()}_bot_${side}`; + setMessagesFn((msgs) => [ + ...msgs, + { + id: botMessageId, + role: 'bot', + content: '', + name: 'Bot', + avatar: logo.src, + timestamp: new Date().toLocaleTimeString(), + isLoading: true + } + ]); + + try { + const chatResponse = await fetch(`${selectedModel.apiURL}/v1/chat/completions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Accept: 'text/event-stream' + }, + body: JSON.stringify(requestData) + }); + + if (!chatResponse.body) { + setIsLoadingFn(false); + setMessagesFn((msgs) => { + const updated = [...msgs]; + const idx = updated.findIndex((m) => m.id === botMessageId); + if (idx !== -1) { + updated[idx].isLoading = false; + updated[idx].content = 'Failed to fetch response from the server.'; + } + return updated; + }); + return; + } + + const reader = chatResponse.body.getReader(); + const textDecoder = new TextDecoder('utf-8'); + let botMessage = ''; + + let done = false; + let firstTokenReceived = false; + while (!done) { + const { value, done: isDone } = await reader.read(); + done = isDone; + if (!value) { + continue; + } + + const chunk = textDecoder.decode(value, { stream: true }); + const lines = chunk.split('\n').filter((line) => line.trim() !== ''); + + for (const line of lines) { + if (line.startsWith('data: ')) { + const json = line.replace('data: ', ''); + if (json === '[DONE]') { + setIsLoadingFn(false); + setMessagesFn((msgs) => { + const updated = [...msgs]; + const idx = updated.findIndex((m) => m.id === botMessageId); + if (idx !== -1) { + updated[idx].isLoading = false; + } + return updated; + }); + return; + } + + try { + const parsed = JSON.parse(json); + const deltaContent = parsed.choices[0].delta?.content; + if (deltaContent) { + if (!firstTokenReceived) { + firstTokenReceived = true; + setMessagesFn((msgs) => { + const updated = [...msgs]; + const idx = updated.findIndex((m) => m.id === botMessageId); + if (idx !== -1) { + updated[idx].isLoading = false; + } + return updated; + }); + } + + botMessage += deltaContent; + handleStreamUpdate(botMessageId, botMessage, setMessagesFn); + } + } catch (err) { + console.error('Error parsing chunk as JSON:', err); + } + } + } + } + + // If stream ends without [DONE] + setIsLoadingFn(false); + setMessagesFn((msgs) => { + const updated = [...msgs]; + const idx = updated.findIndex((m) => m.id === botMessageId); + if (idx !== -1) { + updated[idx].isLoading = false; + } + return updated; + }); + } catch (error) { + console.error('Error fetching chat response:', error); + setIsLoadingFn(false); + setMessagesFn((msgs) => { + const updated = [...msgs]; + const idx = updated.findIndex((m) => m.id === botMessageId); + if (idx !== -1) { + updated[idx].isLoading = false; + updated[idx].content = 'Error fetching chat response'; + } + return updated; + }); + } + }; + + const handleSendLeft = (message: string) => { + handleSend('left', message, selectedModelLeft, setSelectedModelLeft, setQuestionLeft, setIsLoadingLeft, setAlertMessageLeft, setMessagesLeft); + }; + + const handleSendRight = (message: string) => { + handleSend( + 'right', + message, + selectedModelRight, + setSelectedModelRight, + setQuestionRight, + setIsLoadingRight, + setAlertMessageRight, + setMessagesRight + ); + }; + + // Add a copy action to bot messages when they are fully loaded + const transformedMessagesLeft = messagesLeft.map((m) => { + if (m.role === 'bot' && m.content && !m.isLoading) { + return { + ...m, + actions: { + copy: { onClick: () => navigator.clipboard.writeText(m.content || '') } + } + }; + } + return m; + }); + + const transformedMessagesRight = messagesRight.map((m) => { + if (m.role === 'bot' && m.content && !m.isLoading) { + return { + ...m, + actions: { + copy: { onClick: () => navigator.clipboard.writeText(m.content || '') } + } + }; + } + return m; + }); + + // Logs Handling - now calling the Next.js API routes + // Example: /api/fine-tune/jobs/[job_id]/logs + const handleToggleLogsLeft = async (jobId: string, isExpanding: boolean) => { + setExpandedJobsLeft((prev) => ({ ...prev, [jobId]: isExpanding })); + if (isExpanding && !jobLogsLeft[jobId]) { + try { + const response = await fetch(`/api/fine-tune/jobs/${jobId}/logs`, { + method: 'GET' + }); + if (response.ok) { + const logsText = await response.text(); + setJobLogsLeft((prev) => ({ ...prev, [jobId]: logsText })); + } else { + setJobLogsLeft((prev) => ({ ...prev, [jobId]: 'Failed to fetch logs.' })); + } + } catch (error) { + console.error('Error fetching job logs:', error); + setJobLogsLeft((prev) => ({ ...prev, [jobId]: 'Error fetching logs.' })); + } + } + }; + + const handleToggleLogsRight = async (jobId: string, isExpanding: boolean) => { + setExpandedJobsRight((prev) => ({ ...prev, [jobId]: isExpanding })); + if (isExpanding && !jobLogsRight[jobId]) { + try { + const response = await fetch(`/api/fine-tune/jobs/${jobId}/logs`, { + method: 'GET' + }); + if (response.ok) { + const logsText = await response.text(); + setJobLogsRight((prev) => ({ ...prev, [jobId]: logsText })); + } else { + setJobLogsRight((prev) => ({ ...prev, [jobId]: 'Failed to fetch logs.' })); + } + } catch (error) { + console.error('Error fetching job logs:', error); + setJobLogsRight((prev) => ({ ...prev, [jobId]: 'Error fetching logs.' })); + } + } + }; + + return ( +
+ + + Dashboard + Model Chat Evaluation + + + + + Evaluate Two Models Side-by-Side + + +
+ Select a model in each panel to compare responses. For example, compare the base Granite model with a fine-tuned Granite model. +
+
+ +
+ {/* Left Chat */} +
+ + + + + + + Granite fine tune checkpoint + Granite base model + {allModels.map((m) => ( + + {m.name} + + ))} + + + + + + + + + {alertMessageLeft && ( + setAlertMessageLeft(undefined)} title={alertMessageLeft.title}> + {alertMessageLeft.message} + + )} + {messagesLeft.map((msg) => ( + + ))} + + + + handleSendLeft(message)} + hasAttachButton={false} + onChange={(event, val) => setQuestionLeft(val)} + value={questionLeft} + /> + {} } + }} + /> + + + {modelJobIdLeft && ( +
+ handleToggleLogsLeft(modelJobIdLeft, expanded)} + isExpanded={expandedJobsLeft[modelJobIdLeft]} + > + {jobLogsLeft[modelJobIdLeft] ? ( + + {jobLogsLeft[modelJobIdLeft]} + + ) : ( + + )} + +
+ )} +
+ + {/* Right Chat */} +
+ + + + + + + Granite fine tune checkpoint + Granite base model + {allModels.map((m) => ( + + {m.name} + + ))} + + + + + + + + + {alertMessageRight && ( + setAlertMessageRight(undefined)} title={alertMessageRight.title}> + {alertMessageRight.message} + + )} + {messagesRight.map((msg) => ( + + ))} + + + + handleSendRight(message)} + hasAttachButton={false} + onChange={(event, val) => setQuestionRight(val)} + value={questionRight} + /> + {} } + }} + /> + + + {modelJobIdRight && ( +
+ handleToggleLogsRight(modelJobIdRight, expanded)} + isExpanded={expandedJobsRight[modelJobIdRight]} + > + {jobLogsRight[modelJobIdRight] ? ( + + {jobLogsRight[modelJobIdRight]} + + ) : ( + + )} + +
+ )} +
+
+
+ ); +}; + +export default ChatModelEval; diff --git a/src/components/Experimental/FineTuning/index.tsx b/src/components/Experimental/FineTuning/index.tsx new file mode 100644 index 00000000..6eb70806 --- /dev/null +++ b/src/components/Experimental/FineTuning/index.tsx @@ -0,0 +1,469 @@ +// src/components/Experimental/FineTuning/index.tsx +'use client'; + +import React, { useState, useEffect } from 'react'; +import { PageBreadcrumb } from '@patternfly/react-core/dist/dynamic/components/Page'; +import { PageSection } from '@patternfly/react-core/dist/dynamic/components/Page'; +import { Title } from '@patternfly/react-core/dist/dynamic/components/Title'; +import { Breadcrumb, BreadcrumbItem } from '@patternfly/react-core/dist/dynamic/components/Breadcrumb'; +import { Spinner } from '@patternfly/react-core/dist/dynamic/components/Spinner'; +import { Alert } from '@patternfly/react-core/dist/dynamic/components/Alert'; +import { ToggleGroupItem, ToggleGroup } from '@patternfly/react-core'; +import { Button } from '@patternfly/react-core/dist/esm/components/Button'; +import { EmptyState, EmptyStateBody } from '@patternfly/react-core/dist/dynamic/components/EmptyState'; +import { + Modal, + Form, + FormGroup, + Dropdown, + DropdownItem, + DropdownList, + MenuToggle, + MenuToggleElement, + Card, + CardTitle, + CardBody, + CardFooter +} from '@patternfly/react-core'; +import { format } from 'date-fns'; + +import { ExpandableSection, CodeBlock, CodeBlockCode } from '@patternfly/react-core'; + +interface Model { + name: string; + last_modified: string; + size: string; +} + +interface Branch { + name: string; + creationDate: number; +} + +interface Job { + job_id: string; + status: string; + type?: 'generate' | 'train' | 'pipeline'; + branch?: string; + start_time: string; // ISO timestamp + end_time?: string; +} + +const FineTuning: React.FC = () => { + const [models, setModels] = useState([]); + const [branches, setBranches] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [errorMessage, setErrorMessage] = useState(''); + + const [selectedModel, setSelectedModel] = useState(''); + const [selectedBranch, setSelectedBranch] = useState(''); + + const [isModalOpen, setIsModalOpen] = useState(false); + const [isModelDropdownOpen, setIsModelDropdownOpen] = useState(false); + const [isBranchDropdownOpen, setIsBranchDropdownOpen] = useState(false); + + const [jobs, setJobs] = useState([]); + const [selectedStatus, setSelectedStatus] = useState('all'); // 'successful', 'pending', 'failed', 'all' + + // State for managing expanded jobs and their logs + const [expandedJobs, setExpandedJobs] = useState<{ [jobId: string]: boolean }>({}); + const [jobLogs, setJobLogs] = useState<{ [jobId: string]: string }>({}); + + const mapJobType = (job: Job) => { + let jobType: 'generate' | 'train' | 'pipeline' | 'model-serve'; + if (job.job_id.startsWith('g-')) { + jobType = 'generate'; + } else if (job.job_id.startsWith('p-')) { + jobType = 'pipeline'; + } else if (job.job_id.startsWith('ml-')) { + jobType = 'model-serve'; + } else { + jobType = 'train'; + } + return { ...job, type: jobType }; + }; + + // Fetch models, branches, and jobs when the component mounts + useEffect(() => { + const fetchData = async () => { + try { + // Fetch models + const modelsResponse = await fetch('/api/fine-tune/models'); + if (!modelsResponse.ok) { + throw new Error('Failed to fetch models'); + } + const modelsData = await modelsResponse.json(); + + // Fetch branches + const branchesResponse = await fetch('/api/local/git/branches'); + if (!branchesResponse.ok) { + throw new Error('Failed to fetch git branches'); + } + const branchesData = await branchesResponse.json(); + + // Fetch jobs + const jobsResponse = await fetch('/api/fine-tune/jobs'); + if (!jobsResponse.ok) { + throw new Error('Failed to fetch jobs'); + } + const jobsData = await jobsResponse.json(); + + const safeJobsData = Array.isArray(jobsData) ? jobsData : []; + const updatedJobs = safeJobsData + .map((job: Job) => mapJobType(job)) + .sort((a, b) => new Date(b.start_time).getTime() - new Date(a.start_time).getTime()); + + setModels(modelsData); + setBranches(branchesData.branches); + setJobs(updatedJobs); + } catch (error) { + console.error('Error fetching data:', error); + setErrorMessage('Error fetching data'); + } finally { + setIsLoading(false); + } + }; + + fetchData(); + + // Polling to update jobs periodically + const interval = setInterval(() => { + fetch('/api/fine-tune/jobs') + .then((res) => res.json()) + .then((data) => { + const safeJobsData = Array.isArray(data) ? data : []; + const updatedJobs = safeJobsData + .map((job: Job) => mapJobType(job)) + .sort((a, b) => new Date(b.start_time).getTime() - new Date(a.start_time).getTime()); + setJobs(updatedJobs); + }) + .catch((error) => { + console.error('Error fetching jobs:', error); + setJobs([]); + }); + }, 10000); + + return () => clearInterval(interval); + }, []); + + const formatDate = (isoDate?: string) => { + if (!isoDate) return 'N/A'; + const date = new Date(isoDate); + return format(date, 'PPpp'); + }; + + const handleToggleChange = (event: React.MouseEvent) => { + const id = event.currentTarget.id; + setSelectedStatus(id); + }; + + const filteredJobs = jobs.filter((job) => { + if (job.job_id.startsWith('ml-')) { + return false; // Exclude model serve jobs from the dashboard list + } + if (selectedStatus === 'successful') return job.status === 'finished'; + if (selectedStatus === 'pending') return job.status === 'running'; + if (selectedStatus === 'failed') return job.status === 'failed'; + return true; // 'all' + }); + + const handleCreateButtonClick = () => { + setIsModalOpen(true); + }; + + const handleModalClose = () => { + setIsModalOpen(false); + setErrorMessage(''); + setSelectedBranch(''); + setSelectedModel(''); + }; + + const handleGenerateClick = async () => { + if (!selectedModel || !selectedBranch) { + setErrorMessage('Please select both a model and a branch.'); + return; + } + setIsModalOpen(false); + try { + const response = await fetch('/api/fine-tune/data/generate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ modelName: selectedModel, branchName: selectedBranch }) + }); + const result = await response.json(); + if (response.ok) { + const newJob: Job = { + job_id: result.job_id, + status: 'running', + type: result.job_id.startsWith('g-') ? 'generate' : result.job_id.startsWith('p-') ? 'pipeline' : 'train' + }; + setJobs((prevJobs) => [...prevJobs, newJob]); + } else { + setErrorMessage('Failed to start generate job'); + } + } catch (error) { + console.error('Error starting generate job:', error); + setErrorMessage('Error starting generate job'); + } + }; + + const handleTrainClick = async () => { + if (!selectedModel || !selectedBranch) { + setErrorMessage('Please select both a model and a branch.'); + return; + } + setIsModalOpen(false); + try { + const response = await fetch('/api/fine-tune/model/train', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ modelName: selectedModel, branchName: selectedBranch }) + }); + const result = await response.json(); + if (response.ok) { + const newJob: Job = { + job_id: result.job_id, + status: 'running', + type: result.job_id.startsWith('g-') ? 'generate' : result.job_id.startsWith('p-') ? 'pipeline' : 'train' + }; + setJobs((prevJobs) => [...prevJobs, newJob]); + } else { + setErrorMessage('Failed to start train job'); + } + } catch (error) { + console.error('Error starting train job:', error); + setErrorMessage('Error starting train job'); + } + }; + + const handlePipelineClick = async () => { + if (!selectedModel || !selectedBranch) { + setErrorMessage('Please select both a model and a branch.'); + return; + } + setIsModalOpen(false); + try { + const response = await fetch('/api/fine-tune/pipeline/generate-train', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ modelName: selectedModel, branchName: selectedBranch }) + }); + const result = await response.json(); + if (response.ok && result.pipeline_job_id) { + // Add the new job to the job list + const newJob: Job = { + job_id: result.pipeline_job_id, + status: 'running', + type: 'pipeline', + branch: selectedBranch + }; + setJobs((prevJobs) => [...prevJobs, newJob]); + } else { + setErrorMessage('Failed to start generate-train pipeline'); + } + } catch (error) { + console.error('Error starting generate-train pipeline job:', error); + setErrorMessage('Error starting generate-train pipeline job'); + } + }; + + const handleToggleLogs = async (jobId: string, isExpanding: boolean) => { + setExpandedJobs((prev) => ({ ...prev, [jobId]: isExpanding })); + // If expanding and logs not fetched yet, fetch them + if (isExpanding && !jobLogs[jobId]) { + try { + const response = await fetch(`/api/fine-tune/jobs/${jobId}/logs`); + if (response.ok) { + const logsText = await response.text(); + setJobLogs((prev) => ({ ...prev, [jobId]: logsText })); + } else { + setJobLogs((prev) => ({ ...prev, [jobId]: 'Failed to fetch logs.' })); + } + } catch (error) { + console.error('Error fetching job logs:', error); + setJobLogs((prev) => ({ ...prev, [jobId]: 'Error fetching logs.' })); + } + } + }; + + if (isLoading) { + return ( + + + + ); + } + + return ( +
+ + + Dashboard + Fine Tuning + + + + + Fine Tuning Jobs + + + + +
+ + + + + + +
+
+ +
+
+ + + {filteredJobs.length > 0 ? ( +
+ {filteredJobs.map((job) => { + const isExpanded = expandedJobs[job.job_id] || false; + const logs = jobLogs[job.job_id]; + return ( + + + {job.type === 'generate' + ? 'Generate Job' + : job.type === 'pipeline' + ? 'Generate & Train Pipeline' + : job.type === 'model-serve' + ? 'Model Serve Job' + : 'Train Job'} + {' '} + +
+

+ Job ID: {job.job_id} +

+

+ Status: {job.status} +

+

+ Branch: {job.branch || 'N/A'} +

+

+ Start Time: {formatDate(job.start_time)} +

+

+ End Time: {formatDate(job.end_time)} +

+
+
+ + {job.status === 'running' ? ( + + ) : job.status === 'finished' ? ( + + ) : job.status === 'failed' ? ( + + ) : null} + + {/* Expandable section for logs */} + handleToggleLogs(job.job_id, expanded)} + isExpanded={isExpanded} + > + {logs ? ( + + {logs} + + ) : ( + + )} + + +
+ ); + })} +
+ ) : ( + + You haven't created any fine-tuning jobs yet. Use the 'Create' button to get started. + + )} +
+ + + {errorMessage && } +
+ + { + setSelectedBranch(value as string); + setIsBranchDropdownOpen(false); + }} + onOpenChange={(isOpen) => setIsBranchDropdownOpen(isOpen)} + toggle={(toggleRef: React.Ref) => ( + setIsBranchDropdownOpen(!isBranchDropdownOpen)} isExpanded={isBranchDropdownOpen}> + {selectedBranch || 'Select a branch'} + + )} + > + + {branches.map((branch) => ( + + {branch.name} + + ))} + + + + + + { + setSelectedModel(value as string); + setIsModelDropdownOpen(false); + }} + onOpenChange={(isOpen) => setIsModelDropdownOpen(isOpen)} + toggle={(toggleRef: React.Ref) => ( + setIsModelDropdownOpen(!isModelDropdownOpen)} isExpanded={isModelDropdownOpen}> + {selectedModel || 'Select a model'} + + )} + > + + {models.map((model) => ( + + {model.name} + + ))} + + + + +
+ + + +
+ +
+
+
+ ); +}; + +export default FineTuning; diff --git a/src/components/app.scss b/src/components/app.scss new file mode 100644 index 00000000..96b865b2 --- /dev/null +++ b/src/components/app.scss @@ -0,0 +1,276 @@ +.chatbot-ui-page { + background-color: #f0f0f0; + font-family: 'Inter', sans-serif; + color: #333; + display: flex; + flex-direction: column; + //align-items: flex-start; + + .pf-c-page__main { + padding: 0; + background-color: transparent; + } + + // Chat Container + .pf-chatbot { + background-color: #ffffff; + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0,0,0,0.1); + max-width: 600px; + width: 100%; + margin: 1rem auto; + display: flex; + flex-direction: column; + overflow: hidden; + + // Header: Similar to ChatGPT's top bar + &__header { + background: #ffffff; + border-bottom: 1px solid #ddd; + padding: 1rem; + display: flex; + align-items: center; + justify-content: space-between; + + // Hide the default title if you prefer a minimal look + .pf-chatbot__header-title { + font-size: 1.2rem; + font-weight: 600; + color: #333; + margin: 0; + } + + .pf-chatbot__header-actions { + display: flex; + gap: 0.5rem; + + button { + background: transparent; + border: none; + box-shadow: none; + cursor: pointer; + + &:hover { + background-color: #f5f5f5; + } + } + } + } + + // Content Area + &__content { + background: #fafafa; + padding: 1.5rem; + flex: 1; + overflow-y: auto; + display: flex; + flex-direction: column; + gap: 1rem; + } + + // Messages Container + .pf-chatbot__message { + display: flex; + align-items: flex-start; + gap: 0.5rem; + + &--user { + .pf-chatbot__message-contents { + background: #dcf0d9; + color: #333; + align-self: flex-end; + } + } + + &--bot { + .pf-chatbot__message-contents { + background: #ffffff; + color: #333; + } + } + + .pf-chatbot__message-contents { + max-width: 90%; + padding: 0.75rem 1rem; + border-radius: 8px; + box-shadow: 0 1px 3px rgba(0,0,0,0.05); + + .pf-chatbot__message-name { + font-weight: bold; + margin-bottom: 0.25rem; + font-size: 0.9rem; + color: #555; + } + + .pf-chatbot__message-response { + font-size: 1rem; + line-height: 1.5; + + p, li { + margin: 0.5rem 0; + } + + code { + background: #f5f5f5; + padding: 0.2rem 0.4rem; + border-radius: 4px; + font-family: Consolas, Monaco, 'Courier New', monospace; + } + } + + // Quick responses styling + .pf-chatbot__message-quick-response { + margin-top: 0.5rem; + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + + .pf-c-label { + background: #e8f4fd; + border: 1px solid #b3d7f2; + cursor: pointer; + &:hover { + background: #d9edf9; + } + } + } + + // Attachments styling + .pf-chatbot__message-attachments-container { + margin-top: 0.5rem; + .pf-chatbot__message-attachment { + background: #f5f5f5; + padding: 0.5rem; + border-radius: 4px; + margin-right: 0.5rem; + } + } + } + + // Avatar styling + .pf-c-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + flex-shrink: 0; + } + } + + // Footer (Message Bar) + &__footer { + background: #ffffff; + border-top: 1px solid #ddd; + padding: 1rem; + display: flex; + flex-direction: column; + gap: 0.5rem; + + .pf-chatbot__message-bar { + display: flex; + align-items: center; + background: #ffffff; + border: 1px solid #ddd; + border-radius: 999px; + padding: 0.5rem 1rem; + gap: 0.5rem; + + &:focus-within { + box-shadow: 0 0 0 2px #cce5ff; + } + + .pf-chatbot__message-bar-input { + flex: 1; + position: relative; + display: flex; + align-items: center; + + .pf-chatbot__message-bar-placeholder { + position: absolute; + pointer-events: none; + color: #aaa; + font-style: italic; + } + + .pf-chatbot__message-textarea { + width: 100%; + outline: none; + border: none; + background: transparent; + font-size: 1rem; + color: #333; + min-height: 24px; + } + } + + .pf-chatbot__message-bar-actions { + display: flex; + align-items: center; + gap: 0.5rem; + + button { + background: transparent; + border: none; + cursor: pointer; + padding: 0.25rem; + border-radius: 4px; + + &:hover { + background: #f5f5f5; + } + + svg { + width: 20px; + height: 20px; + fill: #666; + } + } + } + } + + .pf-chatbot__footnote { + text-align: center; + font-size: 0.75rem; + color: #666; + } + } + + // Scrollbar styling (optional) + &__content::-webkit-scrollbar { + width: 6px; + } + + &__content::-webkit-scrollbar-thumb { + background: #ccc; + border-radius: 3px; + } + + &__content::-webkit-scrollbar-track { + background: #f2f2f2; + } + } +} + +.chatbot-ui-page { + .pf-v6-c-page__main { + overflow: hidden; + } + @media screen and (max-width: 900px) { + --pf-v6-c-page__main-container--MaxHeight: 100%; + } + .pf-v6-c-page__main-container { + @media screen and (min-width: 1024px) { + border-top: 0px; + } + @media screen and (max-width: 900px) { + --pf-v6-c-page__main-container--MarginInlineStart: 0; + --pf-v6-c-page__main-container--MarginInlineEnd: 0; + --pf-v6-c-page__main-container--BorderWidth: 0; + --pf-v6-c-page__main-container--BorderColor: initial; + --pf-v6-c-page__main-container--BorderRadius: none; + } + } +} + +.pf-chatbot--embedded .pf-chatbot__messagebox { + width: 100%; +} diff --git a/src/types/index.ts b/src/types/index.ts index 736124bf..772c1f26 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -5,7 +5,7 @@ export interface Endpoint { modelName: string; } -export interface Message { +export interface ChatMessage { text: string; isUser: boolean; } From 937dc0ea97f3a79b12aa0585a58ca997bd3342ad Mon Sep 17 00:00:00 2001 From: Brent Salisbury Date: Sat, 21 Dec 2024 22:59:22 -0500 Subject: [PATCH 2/4] Split screen chat for model pre/post-train eval Signed-off-by: Brent Salisbury --- package-lock.json | 105 ++++++++ .../Experimental/ChatEval/ChatEval.tsx | 225 +++++++++++++----- .../Experimental/FineTuning/index.tsx | 90 ++++--- src/components/app.scss | 20 +- 4 files changed, 341 insertions(+), 99 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6f2621ad..20620027 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9020,6 +9020,111 @@ "type": "github", "url": "https://github.com/sponsors/wooorm" } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.0.3.tgz", + "integrity": "sha512-Zxl/TwyXVZPCFSf0u2BNj5sE0F2uR6iSKxWpq4Wlk/Sv9Ob6YCKByQTkV2y6BCic+fkabp9190hyrDdPA/dNrw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.0.3.tgz", + "integrity": "sha512-T5+gg2EwpsY3OoaLxUIofmMb7ohAUlcNZW0fPQ6YAutaWJaxt1Z1h+8zdl4FRIOr5ABAAhXtBcpkZNwUcKI2fw==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.0.3.tgz", + "integrity": "sha512-WkAk6R60mwDjH4lG/JBpb2xHl2/0Vj0ZRu1TIzWuOYfQ9tt9NFsIinI1Epma77JVgy81F32X/AeD+B2cBu/YQA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.0.3.tgz", + "integrity": "sha512-gWL/Cta1aPVqIGgDb6nxkqy06DkwJ9gAnKORdHWX1QBbSZZB+biFYPFti8aKIQL7otCE1pjyPaXpFzGeG2OS2w==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.0.3.tgz", + "integrity": "sha512-QQEMwFd8r7C0GxQS62Zcdy6GKx999I/rTO2ubdXEe+MlZk9ZiinsrjwoiBL5/57tfyjikgh6GOU2WRQVUej3UA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.0.3.tgz", + "integrity": "sha512-9TEp47AAd/ms9fPNgtgnT7F3M1Hf7koIYYWCMQ9neOwjbVWJsHZxrFbI3iEDJ8rf1TDGpmHbKxXf2IFpAvheIQ==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.0.3.tgz", + "integrity": "sha512-VNAz+HN4OGgvZs6MOoVfnn41kBzT+M+tB+OK4cww6DNyWS6wKaDpaAm/qLeOUbnMh0oVx1+mg0uoYARF69dJyA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } } } } diff --git a/src/components/Experimental/ChatEval/ChatEval.tsx b/src/components/Experimental/ChatEval/ChatEval.tsx index 785550f4..c7616cb9 100644 --- a/src/components/Experimental/ChatEval/ChatEval.tsx +++ b/src/components/Experimental/ChatEval/ChatEval.tsx @@ -15,7 +15,8 @@ import { ExpandableSection, Spinner, CodeBlock, - CodeBlockCode + CodeBlockCode, + Switch } from '@patternfly/react-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { faBroom } from '@fortawesome/free-solid-svg-icons'; @@ -41,15 +42,22 @@ import logo from '../../../../public/bot-icon-chat-32x32.svg'; import userLogo from '../../../../public/default-avatar.svg'; // TODO: get nextjs app router server side render working with the patternfly chatbot component. -const MODEL_SERVER_IP = 'http://x.x.x.x'; +const MODEL_SERVER_IP = 'http://128.31.20.129'; const ChatModelEval: React.FC = () => { + const [isUnifiedInput, setIsUnifiedInput] = useState(false); + + // States for unified input + const [questionUnified, setQuestionUnified] = useState(''); + + // States for left chat const [questionLeft, setQuestionLeft] = useState(''); const [messagesLeft, setMessagesLeft] = useState([]); const [selectedModelLeft, setSelectedModelLeft] = useState(null); const [alertMessageLeft, setAlertMessageLeft] = useState<{ title: string; message: string; variant: 'danger' | 'info' } | undefined>(undefined); const [isLoadingLeft, setIsLoadingLeft] = useState(false); + // States for right chat const [questionRight, setQuestionRight] = useState(''); const [messagesRight, setMessagesRight] = useState([]); const [selectedModelRight, setSelectedModelRight] = useState(null); @@ -104,6 +112,21 @@ const ChatModelEval: React.FC = () => { fetchDefaultModels(); }, []); + /** + * Helper function to map internal model identifiers to chat model names. + * "granite-base-served" => "pre-train" + * "granite-latest-served" => "post-train" + * Custom models retain their original modelName. + */ + const mapModelName = (modelName: string): string => { + if (modelName === 'granite-base-served') { + return 'pre-train'; + } else if (modelName === 'granite-latest-served') { + return 'post-train'; + } + return modelName; + }; + const handleServeModel = async (endpoint: string, side: 'left' | 'right') => { // Show loading popup if (side === 'left') { @@ -227,6 +250,10 @@ const ChatModelEval: React.FC = () => { // Common stream update handler const handleStreamUpdate = (id: string, newContent: string, setMessagesFn: React.Dispatch>) => { setMessagesFn((msgs) => { + if (!msgs) { + console.error('msgs is undefined in handleStreamUpdate'); + return []; + } const updated = [...msgs]; const idx = updated.findIndex((m) => m.id === id); if (idx !== -1) { @@ -241,7 +268,6 @@ const ChatModelEval: React.FC = () => { message: string, selectedModel: Model | null, setSelectedModelFn: React.Dispatch>, - setQuestionFn: React.Dispatch>, setIsLoadingFn: React.Dispatch>, setAlertMessageFn: React.Dispatch>, setMessagesFn: React.Dispatch> @@ -277,7 +303,12 @@ const ChatModelEval: React.FC = () => { } ]); - setQuestionFn(''); + if (side === 'left') { + setQuestionLeft(''); + } else { + setQuestionRight(''); + } + setIsLoadingFn(true); const messagesPayload = [ @@ -285,8 +316,11 @@ const ChatModelEval: React.FC = () => { { role: 'user', content: trimmedMessage } ]; + // Map internal model identifiers to chat model names + const chatModelName = mapModelName(selectedModel.modelName); + const requestData = { - model: selectedModel.modelName, + model: chatModelName, messages: messagesPayload, stream: true }; @@ -413,20 +447,11 @@ const ChatModelEval: React.FC = () => { }; const handleSendLeft = (message: string) => { - handleSend('left', message, selectedModelLeft, setSelectedModelLeft, setQuestionLeft, setIsLoadingLeft, setAlertMessageLeft, setMessagesLeft); + handleSend('left', message, selectedModelLeft, setSelectedModelLeft, setIsLoadingLeft, setAlertMessageLeft, setMessagesLeft); }; const handleSendRight = (message: string) => { - handleSend( - 'right', - message, - selectedModelRight, - setSelectedModelRight, - setQuestionRight, - setIsLoadingRight, - setAlertMessageRight, - setMessagesRight - ); + handleSend('right', message, selectedModelRight, setSelectedModelRight, setIsLoadingRight, setAlertMessageRight, setMessagesRight); }; // Add a copy action to bot messages when they are fully loaded @@ -496,8 +521,44 @@ const ChatModelEval: React.FC = () => { } }; + const handleUnifiedSend = (message: string) => { + const trimmedMessage = message.trim(); + if (!trimmedMessage) return; + + setQuestionUnified(''); + // Send to both models if both are selected + let shouldSendLeft = true; + let shouldSendRight = true; + + if (!selectedModelLeft) { + setAlertMessageLeft({ + title: 'No Model Selected', + message: 'Please select a model for the left panel.', + variant: 'danger' + }); + shouldSendLeft = false; + } + + if (!selectedModelRight) { + setAlertMessageRight({ + title: 'No Model Selected', + message: 'Please select a model for the right panel.', + variant: 'danger' + }); + shouldSendRight = false; + } + + if (shouldSendLeft) { + handleSendLeft(trimmedMessage); + } + + if (shouldSendRight) { + handleSendRight(trimmedMessage); + } + }; + return ( -
+
Dashboard @@ -510,17 +571,29 @@ const ChatModelEval: React.FC = () => {
- Select a model in each panel to compare responses. For example, compare the base Granite model with a fine-tuned Granite model. + Select a model in each panel to compare responses. You can toggle between using a single input box that sends to both models or using two + separate input boxes.
+ {/* Toggle Switch */} + + setIsUnifiedInput(!isUnifiedInput)} + /> + +
{/* Left Chat */}
- + - + Granite fine tune checkpoint @@ -537,7 +610,7 @@ const ChatModelEval: React.FC = () => { - + {alertMessageLeft && ( @@ -545,28 +618,31 @@ const ChatModelEval: React.FC = () => { {alertMessageLeft.message} )} - {messagesLeft.map((msg) => ( + {transformedMessagesLeft.map((msg) => ( ))} - - handleSendLeft(message)} - hasAttachButton={false} - onChange={(event, val) => setQuestionLeft(val)} - value={questionLeft} - /> - {} } - }} - /> - + {!isUnifiedInput && ( + + handleSendLeft(message)} + hasAttachButton={false} + onChange={(event, val) => setQuestionLeft(val)} + value={questionLeft} + placeholder="Type your prompt for the left model..." + /> + {} } + }} + /> + + )} {modelJobIdLeft && (
@@ -588,11 +664,11 @@ const ChatModelEval: React.FC = () => {
{/* Right Chat */} -
+
- + - + Granite fine tune checkpoint @@ -609,7 +685,7 @@ const ChatModelEval: React.FC = () => { - + {alertMessageRight && ( @@ -617,28 +693,31 @@ const ChatModelEval: React.FC = () => { {alertMessageRight.message} )} - {messagesRight.map((msg) => ( + {transformedMessagesRight.map((msg) => ( ))} - - handleSendRight(message)} - hasAttachButton={false} - onChange={(event, val) => setQuestionRight(val)} - value={questionRight} - /> - {} } - }} - /> - + {!isUnifiedInput && ( + + handleSendRight(message)} + hasAttachButton={false} + onChange={(event, val) => setQuestionRight(val)} + value={questionRight} + placeholder="Type your prompt for the right model..." + /> + {} } + }} + /> + + )} {modelJobIdRight && (
@@ -659,6 +738,30 @@ const ChatModelEval: React.FC = () => { )}
+ + {/* Unified MessageBar for both models */} + {isUnifiedInput && ( + + + handleUnifiedSend(message)} + hasAttachButton={false} + onChange={(event, val) => setQuestionUnified(val)} + value={questionUnified} + placeholder="Type your prompt here and send to both models..." + /> + {} } + }} + /> + + + )}
); }; diff --git a/src/components/Experimental/FineTuning/index.tsx b/src/components/Experimental/FineTuning/index.tsx index 6eb70806..1383177c 100644 --- a/src/components/Experimental/FineTuning/index.tsx +++ b/src/components/Experimental/FineTuning/index.tsx @@ -1,7 +1,7 @@ // src/components/Experimental/FineTuning/index.tsx 'use client'; -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useRef } from 'react'; import { PageBreadcrumb } from '@patternfly/react-core/dist/dynamic/components/Page'; import { PageSection } from '@patternfly/react-core/dist/dynamic/components/Page'; import { Title } from '@patternfly/react-core/dist/dynamic/components/Title'; @@ -69,6 +69,9 @@ const FineTuning: React.FC = () => { const [expandedJobs, setExpandedJobs] = useState<{ [jobId: string]: boolean }>({}); const [jobLogs, setJobLogs] = useState<{ [jobId: string]: string }>({}); + // Ref to store intervals for each job's logs + const logsIntervals = useRef<{ [jobId: string]: NodeJS.Timeout }>({}); + const mapJobType = (job: Job) => { let jobType: 'generate' | 'train' | 'pipeline' | 'model-serve'; if (job.job_id.startsWith('g-')) { @@ -88,21 +91,21 @@ const FineTuning: React.FC = () => { const fetchData = async () => { try { // Fetch models - const modelsResponse = await fetch('/api/fine-tune/models'); + const modelsResponse = await fetch('/api/fine-tune/models', { cache: 'no-cache' }); if (!modelsResponse.ok) { throw new Error('Failed to fetch models'); } const modelsData = await modelsResponse.json(); // Fetch branches - const branchesResponse = await fetch('/api/local/git/branches'); + const branchesResponse = await fetch('/api/local/git/branches', { cache: 'no-cache' }); if (!branchesResponse.ok) { throw new Error('Failed to fetch git branches'); } const branchesData = await branchesResponse.json(); // Fetch jobs - const jobsResponse = await fetch('/api/fine-tune/jobs'); + const jobsResponse = await fetch('/api/fine-tune/jobs', { cache: 'no-cache' }); if (!jobsResponse.ok) { throw new Error('Failed to fetch jobs'); } @@ -128,7 +131,7 @@ const FineTuning: React.FC = () => { // Polling to update jobs periodically const interval = setInterval(() => { - fetch('/api/fine-tune/jobs') + fetch('/api/fine-tune/jobs', { cache: 'no-cache' }) .then((res) => res.json()) .then((data) => { const safeJobsData = Array.isArray(data) ? data : []; @@ -146,6 +149,13 @@ const FineTuning: React.FC = () => { return () => clearInterval(interval); }, []); + // Clean up all intervals on component unmount + useEffect(() => { + return () => { + Object.values(logsIntervals.current).forEach(clearInterval); + }; + }, []); + const formatDate = (isoDate?: string) => { if (!isoDate) return 'N/A'; const date = new Date(isoDate); @@ -188,7 +198,8 @@ const FineTuning: React.FC = () => { const response = await fetch('/api/fine-tune/data/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ modelName: selectedModel, branchName: selectedBranch }) + body: JSON.stringify({ modelName: selectedModel, branchName: selectedBranch }), + cache: 'no-cache' }); const result = await response.json(); if (response.ok) { @@ -217,7 +228,8 @@ const FineTuning: React.FC = () => { const response = await fetch('/api/fine-tune/model/train', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ modelName: selectedModel, branchName: selectedBranch }) + body: JSON.stringify({ modelName: selectedModel, branchName: selectedBranch }), + cache: 'no-cache' }); const result = await response.json(); if (response.ok) { @@ -246,7 +258,8 @@ const FineTuning: React.FC = () => { const response = await fetch('/api/fine-tune/pipeline/generate-train', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ modelName: selectedModel, branchName: selectedBranch }) + body: JSON.stringify({ modelName: selectedModel, branchName: selectedBranch }), + cache: 'no-cache' }); const result = await response.json(); if (response.ok && result.pipeline_job_id) { @@ -267,21 +280,46 @@ const FineTuning: React.FC = () => { } }; - const handleToggleLogs = async (jobId: string, isExpanding: boolean) => { + const fetchJobLogs = async (jobId: string) => { + try { + const response = await fetch(`/api/fine-tune/jobs/${jobId}/logs`, { + headers: { + 'Cache-Control': 'no-cache' + }, + cache: 'no-cache' + }); + + if (response.ok) { + const logsText = await response.text(); + setJobLogs((prev) => ({ ...prev, [jobId]: logsText })); + } else { + setJobLogs((prev) => ({ ...prev, [jobId]: 'Failed to fetch logs.' })); + } + } catch (error) { + console.error('Error fetching job logs:', error); + setJobLogs((prev) => ({ ...prev, [jobId]: 'Error fetching logs.' })); + } + }; + + const handleToggleLogs = (jobId: string, isExpanding: boolean) => { setExpandedJobs((prev) => ({ ...prev, [jobId]: isExpanding })); - // If expanding and logs not fetched yet, fetch them - if (isExpanding && !jobLogs[jobId]) { - try { - const response = await fetch(`/api/fine-tune/jobs/${jobId}/logs`); - if (response.ok) { - const logsText = await response.text(); - setJobLogs((prev) => ({ ...prev, [jobId]: logsText })); - } else { - setJobLogs((prev) => ({ ...prev, [jobId]: 'Failed to fetch logs.' })); - } - } catch (error) { - console.error('Error fetching job logs:', error); - setJobLogs((prev) => ({ ...prev, [jobId]: 'Error fetching logs.' })); + + if (isExpanding) { + // Fetch logs immediately + fetchJobLogs(jobId); + + // Set up interval to fetch logs every 10 seconds + const intervalId = setInterval(() => { + fetchJobLogs(jobId); + }, 10000); + + // Store the interval ID + logsIntervals.current[jobId] = intervalId; + } else { + // Clear the interval if it exists + if (logsIntervals.current[jobId]) { + clearInterval(logsIntervals.current[jobId]); + delete logsIntervals.current[jobId]; } } }; @@ -361,9 +399,7 @@ const FineTuning: React.FC = () => {
- {job.status === 'running' ? ( - - ) : job.status === 'finished' ? ( + {job.status === 'finished' ? ( ) : job.status === 'failed' ? ( @@ -375,12 +411,10 @@ const FineTuning: React.FC = () => { onToggle={(_event, expanded) => handleToggleLogs(job.job_id, expanded)} isExpanded={isExpanded} > - {logs ? ( + {logs && ( {logs} - ) : ( - )} diff --git a/src/components/app.scss b/src/components/app.scss index 96b865b2..eccff99e 100644 --- a/src/components/app.scss +++ b/src/components/app.scss @@ -11,7 +11,7 @@ background-color: transparent; } - // Chat Container + /* Chat Container */ .pf-chatbot { background-color: #ffffff; border-radius: 8px; @@ -23,7 +23,7 @@ flex-direction: column; overflow: hidden; - // Header: Similar to ChatGPT's top bar + /* Header: Similar to ChatGPT's top bar */ &__header { background: #ffffff; border-bottom: 1px solid #ddd; @@ -32,7 +32,7 @@ align-items: center; justify-content: space-between; - // Hide the default title if you prefer a minimal look + /* Hide the default title if you prefer a minimal look */ .pf-chatbot__header-title { font-size: 1.2rem; font-weight: 600; @@ -57,7 +57,7 @@ } } - // Content Area + /* Content Area */ &__content { background: #fafafa; padding: 1.5rem; @@ -68,7 +68,7 @@ gap: 1rem; } - // Messages Container + /* Messages Container */ .pf-chatbot__message { display: flex; align-items: flex-start; @@ -118,7 +118,7 @@ } } - // Quick responses styling + /* Quick responses styling */ .pf-chatbot__message-quick-response { margin-top: 0.5rem; display: flex; @@ -135,7 +135,7 @@ } } - // Attachments styling + /* Attachments styling */ .pf-chatbot__message-attachments-container { margin-top: 0.5rem; .pf-chatbot__message-attachment { @@ -147,7 +147,7 @@ } } - // Avatar styling + /* Avatar styling */ .pf-c-avatar { width: 40px; height: 40px; @@ -156,7 +156,7 @@ } } - // Footer (Message Bar) + /* Footer (Message Bar) */ &__footer { background: #ffffff; border-top: 1px solid #ddd; @@ -234,7 +234,7 @@ } } - // Scrollbar styling (optional) + /* Scrollbar styling (optional) */ &__content::-webkit-scrollbar { width: 6px; } From b8c6c1e04d9ec3188decc95a29c7471d922b9405 Mon Sep 17 00:00:00 2001 From: Brent Salisbury Date: Mon, 20 Jan 2025 00:11:58 -0500 Subject: [PATCH 3/4] vllm serving capabilities Signed-off-by: Brent Salisbury --- src/Containerfile | 13 +- src/app/api/envConfig/route.ts | 4 +- src/app/api/fine-tune/git/branches/route.ts | 65 ++ src/app/api/fine-tune/model/train/route.ts | 17 +- .../api/fine-tune/model/vllm-status/route.ts | 31 + .../pipeline/generate-train/route.ts | 46 +- src/app/api/local/clone-repo/route.ts | 45 - src/app/api/local/git/branches/route.ts | 142 ---- src/app/api/native/clone-repo/route.ts | 41 + src/app/api/native/git/branches/route.ts | 358 ++++++++ .../api/native/git/knowledge-files/route.ts | 212 +++++ .../{local => native}/pr/knowledge/route.ts | 13 +- .../api/{local => native}/pr/skill/route.ts | 25 +- src/app/api/native/upload/route.ts | 98 +++ src/app/api/tree/route.ts | 8 +- src/app/api/upload/route.ts | 2 +- src/app/contribute/knowledge/page.tsx | 29 +- src/app/contribute/skill/page.tsx | 29 +- src/app/dashboard/page.tsx | 24 +- .../configuration-local/page.tsx | 14 - .../contribute-local/knowledge/page.tsx | 14 - .../contribute-local/skill/page.tsx | 14 - src/app/experimental/dashboard-local/page.tsx | 17 - .../{locallogin.tsx => devmodelogin.tsx} | 10 +- src/app/login/githublogin.tsx | 2 +- src/app/login/nativelogin.tsx | 146 ++++ src/app/login/page.tsx | 27 +- src/app/page.tsx | 4 +- src/components/AppLayout.tsx | 21 +- .../EditKnowledge/EditKnowledge.tsx | 9 +- .../Contribute/EditSkill/EditSkill.tsx | 10 +- .../AttributionInformation.tsx | 2 +- .../Contribute/Knowledge/AutoFill.ts | 16 +- .../DownloadAttribution.tsx | 17 +- .../DownloadDropdown/DownloadDropdown.tsx | 17 +- .../Knowledge/DownloadYaml/DownloadYaml.tsx | 18 +- .../DocumentInformation.tsx | 114 ++- .../Knowledge/{ => Github}/Submit/Submit.tsx | 6 +- .../Knowledge/{ => Github}/Update/Update.tsx | 6 +- .../Knowledge/Github}/index.tsx | 195 ++--- .../KnowledgeInformation.tsx | 2 +- .../KnowledgeQuestionAnswerPairs.tsx | 4 +- .../KnowledgeSeedExample.tsx | 6 +- .../DocumentInformation.tsx | 408 +++++++++ .../KnowledgeQuestionAnswerPairs.tsx | 448 ++++++++++ .../KnowledgeSeedExampleNative.tsx | 87 ++ .../Knowledge/Native/Submit}/Submit.tsx | 10 +- .../Contribute/Knowledge/Native/index.tsx | 800 ++++++++++++++++++ .../Knowledge}/ReviewSubmission/index.tsx | 50 +- .../Contribute/Knowledge/UploadFile.tsx | 10 +- .../Knowledge/ViewDropdown/ViewDropdown.tsx | 43 +- src/components/Contribute/Knowledge/index.tsx | 658 -------------- .../Contribute/Knowledge/knowledge.css | 16 + .../Contribute/Knowledge/validation.tsx | 20 +- .../AttributionInformation.tsx | 2 +- src/components/Contribute/Skill/AutoFill.ts | 4 +- .../DownloadAttribution.tsx | 2 +- .../DownloadDropdown/DownloadDropdown.tsx | 2 +- .../Skill/DownloadYaml/DownloadYaml.tsx | 3 +- .../Skill/{ => Github}/Submit/Submit.tsx | 6 +- .../Skill/{ => Github}/Update/Update.tsx | 6 +- .../Contribute/Skill/{ => Github}/index.tsx | 140 ++- .../Contribute/Skill/{ => Github}/skills.css | 0 .../Skill/Native/Submit/Submit.tsx} | 8 +- .../Skill/Native}/index.tsx | 159 ++-- .../Skill/Native}/skills.css | 0 .../SkillsInformation/SkillsInformation.tsx | 2 +- .../SkillsSeedExample/SkillsSeedExample.tsx | 6 +- .../Skill/ViewDropdown/ViewDropdown.tsx | 3 +- .../Contribute/Skill/validation.tsx | 5 +- .../{index.tsx => Github/dashboard.tsx} | 13 +- src/components/Dashboard/Native/dashboard.tsx | 494 +++++++++++ .../Experimental/ChatEval/ChatEval.tsx | 324 +++---- .../CloneRepoLocal/CloneRepoLocal.module.css | 9 - .../CloneRepoLocal/CloneRepoLocal.tsx | 115 --- .../ContributeLocal/Knowledge/knowledge.css | 39 - .../Experimental/DashboardLocal/index.tsx | 211 ----- .../Experimental/FineTuning/index.tsx | 156 +++- src/components/GithubAccessPopup/index.tsx | 2 +- .../ModelServeStatus/ModelServeStatus.tsx | 75 ++ src/components/PathService/PathService.tsx | 20 +- src/components/YamlCodeModal/index.tsx | 30 +- src/components/styles/globals.scss | 7 + src/components/styles/tokens-charts-dark.scss | 173 ++++ src/components/styles/tokens-charts.scss | 173 ++++ src/components/styles/tokens-dark.scss | 384 +++++++++ src/components/styles/tokens-default.scss | 689 +++++++++++++++ src/components/styles/tokens-palette.scss | 88 ++ src/healthcheck-probe.sh | 36 + src/types/index.ts | 77 +- 90 files changed, 5877 insertions(+), 2029 deletions(-) create mode 100644 src/app/api/fine-tune/git/branches/route.ts create mode 100644 src/app/api/fine-tune/model/vllm-status/route.ts delete mode 100644 src/app/api/local/clone-repo/route.ts delete mode 100644 src/app/api/local/git/branches/route.ts create mode 100644 src/app/api/native/clone-repo/route.ts create mode 100644 src/app/api/native/git/branches/route.ts create mode 100644 src/app/api/native/git/knowledge-files/route.ts rename src/app/api/{local => native}/pr/knowledge/route.ts (78%) rename src/app/api/{local => native}/pr/skill/route.ts (67%) create mode 100644 src/app/api/native/upload/route.ts delete mode 100644 src/app/experimental/contribute-local/configuration-local/page.tsx delete mode 100644 src/app/experimental/contribute-local/knowledge/page.tsx delete mode 100644 src/app/experimental/contribute-local/skill/page.tsx delete mode 100644 src/app/experimental/dashboard-local/page.tsx rename src/app/login/{locallogin.tsx => devmodelogin.tsx} (96%) create mode 100644 src/app/login/nativelogin.tsx rename src/components/Contribute/Knowledge/{ => Github}/DocumentInformation/DocumentInformation.tsx (81%) rename src/components/Contribute/Knowledge/{ => Github}/Submit/Submit.tsx (95%) rename src/components/Contribute/Knowledge/{ => Github}/Update/Update.tsx (97%) rename src/components/{Experimental/ContributeLocal/Knowledge => Contribute/Knowledge/Github}/index.tsx (80%) create mode 100644 src/components/Contribute/Knowledge/Native/DocumentInformation/DocumentInformation.tsx create mode 100644 src/components/Contribute/Knowledge/Native/KnowledgeQuestionAnswerPairsNative/KnowledgeQuestionAnswerPairs.tsx create mode 100644 src/components/Contribute/Knowledge/Native/KnowledgeSeedExampleNative/KnowledgeSeedExampleNative.tsx rename src/components/{Experimental/ContributeLocal/Knowledge/SubmitLocal => Contribute/Knowledge/Native/Submit}/Submit.tsx (92%) create mode 100644 src/components/Contribute/Knowledge/Native/index.tsx rename src/components/{Experimental => Contribute/Knowledge}/ReviewSubmission/index.tsx (64%) delete mode 100644 src/components/Contribute/Knowledge/index.tsx rename src/components/Contribute/Skill/{ => Github}/Submit/Submit.tsx (95%) rename src/components/Contribute/Skill/{ => Github}/Update/Update.tsx (96%) rename src/components/Contribute/Skill/{ => Github}/index.tsx (79%) rename src/components/Contribute/Skill/{ => Github}/skills.css (100%) rename src/components/{Experimental/ContributeLocal/Skill/SubmitLocal/SubmitLocal.tsx => Contribute/Skill/Native/Submit/Submit.tsx} (93%) rename src/components/{Experimental/ContributeLocal/Skill => Contribute/Skill/Native}/index.tsx (75%) rename src/components/{Experimental/ContributeLocal/Skill => Contribute/Skill/Native}/skills.css (100%) rename src/components/Dashboard/{index.tsx => Github/dashboard.tsx} (96%) create mode 100644 src/components/Dashboard/Native/dashboard.tsx delete mode 100644 src/components/Experimental/CloneRepoLocal/CloneRepoLocal.module.css delete mode 100644 src/components/Experimental/CloneRepoLocal/CloneRepoLocal.tsx delete mode 100644 src/components/Experimental/ContributeLocal/Knowledge/knowledge.css delete mode 100644 src/components/Experimental/DashboardLocal/index.tsx create mode 100644 src/components/ModelServeStatus/ModelServeStatus.tsx create mode 100644 src/components/styles/globals.scss create mode 100644 src/components/styles/tokens-charts-dark.scss create mode 100644 src/components/styles/tokens-charts.scss create mode 100644 src/components/styles/tokens-dark.scss create mode 100644 src/components/styles/tokens-default.scss create mode 100644 src/components/styles/tokens-palette.scss create mode 100755 src/healthcheck-probe.sh diff --git a/src/Containerfile b/src/Containerfile index 3e2e0989..383b6289 100644 --- a/src/Containerfile +++ b/src/Containerfile @@ -1,14 +1,17 @@ FROM registry.access.redhat.com/ubi9/nodejs-22:9.5-1730543890 +USER root + WORKDIR /opt/app-root/src -COPY package*.json ./ +COPY ./ . + +RUN dnf install -y jq +RUN mkdir -p node_modules +RUN chown -R default:root package*.json next-env.d.ts node_modules /opt/app-root/src/src/healthcheck-probe.sh -USER root -RUN chown -R default:root /opt/app-root/src/package*.json USER default -RUN npm install -COPY ./ . +RUN npm install RUN npm run build CMD ["npm", "run", "start"] diff --git a/src/app/api/envConfig/route.ts b/src/app/api/envConfig/route.ts index c58b2a65..9381ed2b 100644 --- a/src/app/api/envConfig/route.ts +++ b/src/app/api/envConfig/route.ts @@ -15,7 +15,9 @@ export async function GET() { UPSTREAM_REPO_OWNER: process.env.NEXT_PUBLIC_TAXONOMY_REPO_OWNER || '', UPSTREAM_REPO_NAME: process.env.NEXT_PUBLIC_TAXONOMY_REPO || '', DEPLOYMENT_TYPE: process.env.IL_UI_DEPLOYMENT || '', - EXPERIMENTAL_FEATURES: process.env.NEXT_PUBLIC_EXPERIMENTAL_FEATURES || '' + ENABLE_DEV_MODE: process.env.IL_ENABLE_DEV_MODE || 'false', + EXPERIMENTAL_FEATURES: process.env.NEXT_PUBLIC_EXPERIMENTAL_FEATURES || '', + TAXONOMY_ROOT_DIR: process.env.NEXT_PUBLIC_TAXONOMY_ROOT_DIR || '' }; return NextResponse.json(envConfig); diff --git a/src/app/api/fine-tune/git/branches/route.ts b/src/app/api/fine-tune/git/branches/route.ts new file mode 100644 index 00000000..ccb1902a --- /dev/null +++ b/src/app/api/fine-tune/git/branches/route.ts @@ -0,0 +1,65 @@ +// src/app/api/native/fine-tune/git/branches/route.ts +import { NextResponse } from 'next/server'; +import * as git from 'isomorphic-git'; +import fs from 'fs'; +import path from 'path'; + +const REMOTE_TAXONOMY_ROOT_DIR = process.env.NEXT_PUBLIC_TAXONOMY_ROOT_DIR || ''; + +export async function GET() { + const REPO_DIR = path.join(REMOTE_TAXONOMY_ROOT_DIR, '/taxonomy'); + try { + console.log(`Checking local taxonomy directory for branches: ${REPO_DIR}`); + + // Ensure the repository path exists + if (!fs.existsSync(REPO_DIR)) { + console.log('Local repository path does not exist:', REPO_DIR); + return NextResponse.json({ error: 'Local repository path does not exist.' }, { status: 400 }); + } + + console.log('Local taxonomy directory exists. Proceeding with branch listing.'); + + // List all branches in the repository + const branches = await git.listBranches({ fs, dir: REPO_DIR }); + console.log(`Branches found: ${branches.join(', ')}`); + + const branchDetails = []; + + for (const branch of branches) { + const branchCommit = await git.resolveRef({ + fs, + dir: REPO_DIR, + ref: branch + }); + const commitDetails = await git.readCommit({ + fs, + dir: REPO_DIR, + oid: branchCommit + }); + + const commitMessage = commitDetails.commit.message; + + // Check for Signed-off-by line + const signoffMatch = commitMessage.match(/^Signed-off-by: (.+)$/m); + const signoff = signoffMatch ? signoffMatch[1] : null; + const messageStr = commitMessage.split('Signed-off-by'); + + branchDetails.push({ + name: branch, + creationDate: commitDetails.commit.committer.timestamp * 1000, + message: messageStr[0].replace(/\n+$/, ''), + author: signoff + }); + } + + // Sort by creation date, newest first + branchDetails.sort((a, b) => b.creationDate - a.creationDate); + + console.log('Total branches present in native taxonomy (fine-tune):', branchDetails.length); + + return NextResponse.json({ branches: branchDetails }, { status: 200 }); + } catch (error) { + console.error('Failed to list branches from local taxonomy (fine-tune):', error); + return NextResponse.json({ error: 'Failed to list branches from local taxonomy (fine-tune)' }, { status: 500 }); + } +} diff --git a/src/app/api/fine-tune/model/train/route.ts b/src/app/api/fine-tune/model/train/route.ts index aba8809d..a910cd9f 100644 --- a/src/app/api/fine-tune/model/train/route.ts +++ b/src/app/api/fine-tune/model/train/route.ts @@ -1,4 +1,4 @@ -// src/app/api/fine-tune/model/train +// src/app/api/fine-tune/model/train/route.ts 'use server'; import { NextResponse } from 'next/server'; @@ -8,16 +8,21 @@ export async function POST(request: Request) { console.log('Received train job request'); // Parse the request body for required data - const { modelName, branchName } = await request.json(); + const { modelName, branchName, epochs } = await request.json(); const API_SERVER = process.env.NEXT_PUBLIC_API_SERVER!; - console.log('Request body:', { modelName, branchName }); + console.log('Request body:', { modelName, branchName, epochs }); if (!modelName || !branchName) { console.error('Missing required parameters: modelName and branchName'); return NextResponse.json({ error: 'Missing required parameters: modelName and branchName' }, { status: 400 }); } + // Validate epochs if provided + if (epochs !== undefined && (typeof epochs !== 'number' || epochs <= 0)) { + return NextResponse.json({ error: "'epochs' must be a positive integer" }, { status: 400 }); + } + // Forward the request to the API server const endpoint = `${API_SERVER}/model/train`; @@ -30,7 +35,8 @@ export async function POST(request: Request) { }, body: JSON.stringify({ modelName, - branchName + branchName, + epochs }) }); @@ -40,7 +46,8 @@ export async function POST(request: Request) { }); if (!response.ok) { - console.error('Error response from API server:', response.status, response.statusText); + const errorText = await response.text(); + console.error('Error response from API server:', response.status, response.statusText, errorText); return NextResponse.json({ error: 'Failed to train the model on the API server' }, { status: response.status }); } diff --git a/src/app/api/fine-tune/model/vllm-status/route.ts b/src/app/api/fine-tune/model/vllm-status/route.ts new file mode 100644 index 00000000..0ebed909 --- /dev/null +++ b/src/app/api/fine-tune/model/vllm-status/route.ts @@ -0,0 +1,31 @@ +// src/app/api/model/vllm-status/route.ts +'use server'; + +import { NextResponse } from 'next/server'; + +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const modelName = searchParams.get('modelName'); + if (!modelName) { + return NextResponse.json({ error: 'Missing modelName query param' }, { status: 400 }); + } + + const API_SERVER = process.env.NEXT_PUBLIC_API_SERVER!; + const endpoint = `${API_SERVER}/vllm-status?model_name=${modelName}`; + + console.log('Forwarding request to vllm-status:', endpoint); + + const response = await fetch(endpoint); + if (!response.ok) { + console.error('vllm-status error from API server:', response.status, response.statusText); + return NextResponse.json({ error: 'Failed to get vllm status' }, { status: response.status }); + } + + const statusData = await response.json(); + return NextResponse.json(statusData, { status: 200 }); + } catch (error) { + console.error('Unexpected error in vllm-status route:', error); + return NextResponse.json({ error: 'Unexpected error fetching vllm status' }, { status: 500 }); + } +} diff --git a/src/app/api/fine-tune/pipeline/generate-train/route.ts b/src/app/api/fine-tune/pipeline/generate-train/route.ts index 090dc490..d9bdb91f 100644 --- a/src/app/api/fine-tune/pipeline/generate-train/route.ts +++ b/src/app/api/fine-tune/pipeline/generate-train/route.ts @@ -1,3 +1,4 @@ +// src/app/api/fine-tune/pipeline/generate-train/route.ts 'use server'; import { NextResponse } from 'next/server'; @@ -5,13 +6,21 @@ import { NextResponse } from 'next/server'; export async function POST(request: Request) { try { // Parse the request body for required data - const { modelName, branchName } = await request.json(); + const { modelName, branchName, epochs } = await request.json(); const API_SERVER = process.env.NEXT_PUBLIC_API_SERVER!; + console.log('Request body:', { modelName, branchName, epochs }); + if (!modelName || !branchName) { + console.error('Missing required parameters: modelName and branchName'); return NextResponse.json({ error: 'Missing required parameters: modelName and branchName' }, { status: 400 }); } + // Validate epochs if provided + if (epochs !== undefined && (typeof epochs !== 'number' || epochs <= 0)) { + return NextResponse.json({ error: "'epochs' must be a positive integer" }, { status: 400 }); + } + // Forward the request to the API server's pipeline endpoint const endpoint = `${API_SERVER}/pipeline/generate-train`; @@ -22,19 +31,42 @@ export async function POST(request: Request) { }, body: JSON.stringify({ modelName, - branchName + branchName, + epochs }) }); + console.log('Response from API server:', { + status: response.status, + statusText: response.statusText + }); + if (!response.ok) { - console.error('Error response from API server (pipeline):', response.status, response.statusText); - return NextResponse.json({ error: 'Failed to run generate-train pipeline on the API server' }, { status: response.status }); + const errorText = await response.text(); + console.error('Error response from API server:', response.status, response.statusText, errorText); + return NextResponse.json({ error: 'Failed to train the model on the API server' }, { status: response.status }); + } + + const responseData; + try { + const text = await response.text(); + responseData = text ? JSON.parse(text) : {}; + console.log('Parsed response data:', responseData); + } catch (error) { + console.error('Error parsing JSON response from API server:', error); + return NextResponse.json({ error: 'Invalid JSON response from the API server' }, { status: 500 }); + } + + if (!responseData.job_id) { + console.error('Missing job_id in API server response:', responseData); + return NextResponse.json({ error: 'API server response does not contain job_id' }, { status: 500 }); } - const responseData = await response.json(); + // Return the response from the API server to the client + console.log('Returning success response with job_id:', responseData.job_id); return NextResponse.json(responseData, { status: 200 }); } catch (error) { - console.error('Error during generate-train pipeline:', error); - return NextResponse.json({ error: 'An error occurred during generate-train pipeline' }, { status: 500 }); + console.error('Unexpected error during training:', error); + return NextResponse.json({ error: 'An unexpected error occurred during training' }, { status: 500 }); } } diff --git a/src/app/api/local/clone-repo/route.ts b/src/app/api/local/clone-repo/route.ts deleted file mode 100644 index 309582c5..00000000 --- a/src/app/api/local/clone-repo/route.ts +++ /dev/null @@ -1,45 +0,0 @@ -// src/pages/api/clone-repo.ts -import { NextRequest, NextResponse } from 'next/server'; -import * as git from 'isomorphic-git'; -import http from 'isomorphic-git/http/node'; -import fs from 'fs'; -import path from 'path'; - -// Retrieve the base directory from the environment variable -const BASE_DIRECTORY = process.env.NEXT_PUBLIC_BASE_CLONE_DIRECTORY; - -export async function POST(req: NextRequest) { - const { repoUrl, directory } = await req.json(); - - if (!repoUrl || !directory) { - return NextResponse.json({ message: 'Repository URL and directory are required' }, { status: 400 }); - } - - if (!BASE_DIRECTORY) { - return NextResponse.json({ message: 'Base directory is not configured on the server' }, { status: 500 }); - } - - try { - const clonePath = path.resolve(BASE_DIRECTORY, directory); - - // Ensure clonePath is within BASE_DIRECTORY - if (!clonePath.startsWith(BASE_DIRECTORY)) { - return NextResponse.json({ message: 'Invalid directory path' }, { status: 403 }); - } - - await git.clone({ - fs, - http, - dir: clonePath, - url: repoUrl, - singleBranch: true, - depth: 1 - }); - - // Include the full path in the response for client display - return NextResponse.json({ message: `Repository cloned successfully.`, fullPath: clonePath }, { status: 200 }); - } catch (error: unknown) { - const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; - return NextResponse.json({ message: `Failed to clone repository: ${errorMessage}` }, { status: 500 }); - } -} diff --git a/src/app/api/local/git/branches/route.ts b/src/app/api/local/git/branches/route.ts deleted file mode 100644 index 2558f6f2..00000000 --- a/src/app/api/local/git/branches/route.ts +++ /dev/null @@ -1,142 +0,0 @@ -// src/app/api/local/git/branches/route.ts -import { NextRequest, NextResponse } from 'next/server'; -import * as git from 'isomorphic-git'; -import fs from 'fs'; -import path from 'path'; - -// Get the repository path from the environment variable -const REPO_DIR = process.env.NEXT_PUBLIC_LOCAL_REPO_PATH || '/path/to/local/repo'; - -export async function GET() { - try { - // Ensure the repository path exists - if (!fs.existsSync(REPO_DIR)) { - return NextResponse.json({ error: 'Repository path does not exist.' }, { status: 400 }); - } - - // List all branches in the repository - const branches = await git.listBranches({ fs, dir: REPO_DIR }); - const branchDetails = []; - - for (const branch of branches) { - const branchCommit = await git.resolveRef({ fs, dir: REPO_DIR, ref: branch }); - const commitDetails = await git.readCommit({ fs, dir: REPO_DIR, oid: branchCommit }); - - branchDetails.push({ - name: branch, - creationDate: commitDetails.commit.committer.timestamp * 1000 // Convert to milliseconds - }); - } - - branchDetails.sort((a, b) => b.creationDate - a.creationDate); // Sort by creation date, newest first - - return NextResponse.json({ branches: branchDetails }, { status: 200 }); - } catch (error) { - console.error('Failed to list branches:', error); - return NextResponse.json({ error: 'Failed to list branches' }, { status: 500 }); - } -} - -// Handle POST requests for merge or branch comparison -export async function POST(req: NextRequest) { - const { branchName, action } = await req.json(); - - try { - if (action === 'merge') { - // Ensure valid branch name - if (!branchName || branchName === 'main') { - return NextResponse.json({ error: 'Invalid branch name for merge' }, { status: 400 }); - } - - // Initialize the repository and checkout main branch - await git.init({ fs, dir: REPO_DIR }); - await git.checkout({ fs, dir: REPO_DIR, ref: 'main' }); - - // Perform the merge - await git.merge({ - fs, - dir: REPO_DIR, - ours: 'main', - theirs: branchName, - author: { - name: 'Instruct Lab Local', - email: 'local@instructlab.ai' - } - }); - - return NextResponse.json({ message: `Successfully merged ${branchName} into main.` }, { status: 200 }); - } else if (action === 'diff') { - // Ensure valid branch name - if (!branchName || branchName === 'main') { - return NextResponse.json({ error: 'Invalid branch name for comparison' }, { status: 400 }); - } - - // Fetch the commit SHA for `main` and the target branch - const mainCommit = await git.resolveRef({ fs, dir: REPO_DIR, ref: 'main' }); - const branchCommit = await git.resolveRef({ fs, dir: REPO_DIR, ref: branchName }); - - const mainFiles = await getFilesFromTree(mainCommit); - const branchFiles = await getFilesFromTree(branchCommit); - - const changes = []; - - // Identify modified and deleted files - for (const file in mainFiles) { - if (branchFiles[file]) { - if (mainFiles[file] !== branchFiles[file]) { - changes.push({ file, status: 'modified' }); - } - } else { - changes.push({ file, status: 'deleted' }); - } - } - - // Identify added files - for (const file in branchFiles) { - if (!mainFiles[file]) { - changes.push({ file, status: 'added' }); - } - } - - return NextResponse.json({ changes }, { status: 200 }); - } else { - return NextResponse.json({ error: 'Invalid action specified' }, { status: 400 }); - } - } catch (error) { - console.error(`Failed to ${action === 'merge' ? 'merge branch' : 'compare branches'}:`, error); - return NextResponse.json( - { - error: `Failed to ${action === 'merge' ? 'merge branch' : 'compare branches'}` - }, - { status: 500 } - ); - } finally { - // Ensure switching back to 'main' branch after any operation - try { - await git.checkout({ fs, dir: REPO_DIR, ref: 'main' }); - } catch (checkoutError) { - console.error('Failed to switch back to main branch:', checkoutError); - } - } -} - -// Helper function to recursively gather file paths and their oids from a tree -async function getFilesFromTree(commitOid: string) { - const fileMap: Record = {}; - - async function walkTree(dir: string) { - const tree = await git.readTree({ fs, dir: REPO_DIR, oid: commitOid, filepath: dir }); - - for (const entry of tree.tree) { - const fullPath = path.join(dir, entry.path); - if (entry.type === 'blob') { - fileMap[fullPath] = entry.oid; - } else if (entry.type === 'tree') { - await walkTree(fullPath); // Recursively walk subdirectories - } - } - } - - await walkTree(''); - return fileMap; -} diff --git a/src/app/api/native/clone-repo/route.ts b/src/app/api/native/clone-repo/route.ts new file mode 100644 index 00000000..e201529e --- /dev/null +++ b/src/app/api/native/clone-repo/route.ts @@ -0,0 +1,41 @@ +// src/pages/api/clone-repo.ts +import { NextResponse } from 'next/server'; +import * as git from 'isomorphic-git'; +import http from 'isomorphic-git/http/node'; +import fs from 'fs'; +import path from 'path'; + +// Retrieve the base directory from the environment variable +const LOCAL_TAXONOMY_ROOT_DIR = process.env.NEXT_PUBLIC_LOCAL_TAXONOMY_ROOT_DIR || `${process.env.HOME}/.instructlab-ui`; +const TAXONOMY_REPO_URL = process.env.NEXT_PUBLIC_TAXONOMY_REPO_URL || 'https://github.com/instructlab/taxonomy.git'; + +export async function POST() { + const taxonomyDirectoryPath = path.join(LOCAL_TAXONOMY_ROOT_DIR, '/taxonomy'); + + if (fs.existsSync(taxonomyDirectoryPath)) { + const files = fs.readdirSync(taxonomyDirectoryPath); + if (files.length > 0) { + console.log(`Using existing native Taxonomy repository at ${taxonomyDirectoryPath}.`); + return NextResponse.json({ message: `Using existing native Taxonomy repository at ${taxonomyDirectoryPath}.` }, { status: 200 }); + } + fs.rmdirSync(taxonomyDirectoryPath, { recursive: true }); + } + + try { + await git.clone({ + fs, + http, + dir: taxonomyDirectoryPath, + url: TAXONOMY_REPO_URL, + singleBranch: true + }); + + // Include the full path in the response for client display + console.log(`Local Taxonomy repository cloned successfully to ${taxonomyDirectoryPath}.`); + return NextResponse.json({ message: `Local Taxonomy repository cloned successfully to ${taxonomyDirectoryPath}.` }, { status: 200 }); + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; + console.error(`Failed to clone local taxonomy repository: ${errorMessage}`); + return NextResponse.json({ message: `Failed to clone local taxonomy repository: ${errorMessage}` }, { status: 500 }); + } +} diff --git a/src/app/api/native/git/branches/route.ts b/src/app/api/native/git/branches/route.ts new file mode 100644 index 00000000..c6e0e68f --- /dev/null +++ b/src/app/api/native/git/branches/route.ts @@ -0,0 +1,358 @@ +// src/app/api/native/git/branches/route.ts +import { NextRequest, NextResponse } from 'next/server'; +import * as git from 'isomorphic-git'; +import fs from 'fs'; +import path from 'path'; + +// Get the repository path from the environment variable +const LOCAL_TAXONOMY_ROOT_DIR = process.env.NEXT_PUBLIC_LOCAL_TAXONOMY_ROOT_DIR || `${process.env.HOME}/.instructlab-ui`; +const REMOTE_TAXONOMY_ROOT_DIR = process.env.NEXT_PUBLIC_TAXONOMY_ROOT_DIR || ''; +const REMOTE_TAXONOMY_REPO_CONTAINER_MOUNT_DIR = '/tmp/.instructlab-ui'; + +interface Diffs { + file: string; + status: string; + content?: string; +} + +export async function GET() { + const REPO_DIR = path.join(LOCAL_TAXONOMY_ROOT_DIR, '/taxonomy'); + try { + // Ensure the repository path exists + if (!fs.existsSync(REPO_DIR)) { + return NextResponse.json({ error: 'Local repository path does not exist.' }, { status: 400 }); + } + + // List all branches in the repository + const branches = await git.listBranches({ fs, dir: REPO_DIR }); + const branchDetails = []; + + for (const branch of branches) { + const branchCommit = await git.resolveRef({ fs, dir: REPO_DIR, ref: branch }); + const commitDetails = await git.readCommit({ fs, dir: REPO_DIR, oid: branchCommit }); + + const commitMessage = commitDetails.commit.message; + + // Check for Signed-off-by line + const signoffMatch = commitMessage.match(/^Signed-off-by: (.+)$/m); + const signoff = signoffMatch ? signoffMatch[1] : null; + const messageStr = commitMessage.split('Signed-off-by'); + branchDetails.push({ + name: branch, + creationDate: commitDetails.commit.committer.timestamp * 1000, + message: messageStr[0].replace(/\n+$/, ''), + author: signoff + }); + } + + branchDetails.sort((a, b) => b.creationDate - a.creationDate); // Sort by creation date, newest first + console.log('Total branches present in native taxonomy:', branchDetails.length); + + return NextResponse.json({ branches: branchDetails }, { status: 200 }); + } catch (error) { + console.error('Failed to list branches from local taxonomy:', error); + return NextResponse.json({ error: 'Failed to list branches from local taxonomy' }, { status: 500 }); + } +} + +// Handle POST requests for delete/diff/publish actions +export async function POST(req: NextRequest) { + const LOCAL_TAXONOMY_DIR = path.join(LOCAL_TAXONOMY_ROOT_DIR, '/taxonomy'); + const { branchName, action } = await req.json(); + console.log('Received POST request:', { branchName, action }); + + if (action === 'delete') { + return handleDelete(branchName, LOCAL_TAXONOMY_DIR); + } + + if (action === 'diff') { + return handleDiff(branchName, LOCAL_TAXONOMY_DIR); + } + + if (action === 'publish') { + let remoteTaxonomyRepoDirFinal: string = ''; + + const remoteTaxonomyRepoContainerMountDir = path.join(REMOTE_TAXONOMY_REPO_CONTAINER_MOUNT_DIR, '/taxonomy'); + const remoteTaxonomyRepoDir = path.join(REMOTE_TAXONOMY_ROOT_DIR, '/taxonomy'); + + // Check if there is taxonomy repository mounted in the container + if (fs.existsSync(remoteTaxonomyRepoContainerMountDir) && fs.readdirSync(remoteTaxonomyRepoContainerMountDir).length !== 0) { + remoteTaxonomyRepoDirFinal = remoteTaxonomyRepoContainerMountDir; + console.log('Remote taxonomy repository ', remoteTaxonomyRepoDir, ' is mounted at:', remoteTaxonomyRepoDirFinal); + } else { + // If remote taxonomy is not mounted, it means it's local deployment and we can directly use the paths + if (fs.existsSync(remoteTaxonomyRepoDir) && fs.readdirSync(remoteTaxonomyRepoDir).length !== 0) { + remoteTaxonomyRepoDirFinal = remoteTaxonomyRepoDir; + } + } + if (remoteTaxonomyRepoDirFinal === '') { + return NextResponse.json({ error: 'Remote taxonomy repository path does not exist.' }, { status: 400 }); + } + + console.log('Remote taxonomy repository path:', remoteTaxonomyRepoDirFinal); + + return handlePublish(branchName, LOCAL_TAXONOMY_DIR, remoteTaxonomyRepoDirFinal); + } + return NextResponse.json({ error: 'Invalid action specified' }, { status: 400 }); +} + +async function handleDelete(branchName: string, localTaxonomyDir: string) { + try { + if (!branchName || branchName === 'main') { + return NextResponse.json({ error: 'Invalid branch name for deletion' }, { status: 400 }); + } + + // Delete the target branch + await git.deleteBranch({ fs, dir: localTaxonomyDir, ref: branchName }); + + return NextResponse.json({ message: `Successfully deleted contribution ${branchName}.` }, { status: 200 }); + } catch (error) { + console.error(`Failed to delete contribution ${branchName}:`, error); + return NextResponse.json( + { + error: `Failed to delete contribution ${branchName}` + }, + { status: 500 } + ); + } finally { + // Ensure switching back to 'main' branch after any operation + try { + await git.checkout({ fs, dir: localTaxonomyDir, ref: 'main' }); + } catch (checkoutError) { + console.error('Failed to switch back to main branch:', checkoutError); + } + } +} + +async function handleDiff(branchName: string, localTaxonomyDir: string) { + try { + // Ensure valid branch name + if (!branchName || branchName === 'main') { + return NextResponse.json({ error: 'Invalid branch name for comparison' }, { status: 400 }); + } + + const changes = await findDiff(branchName, localTaxonomyDir); + const enrichedChanges: Diffs[] = []; + for (const change of changes) { + if (change.status === 'added' || change.status === 'modified') { + const fileContent = await readFileFromBranch(localTaxonomyDir, branchName, change.file); + enrichedChanges.push({ ...change, content: fileContent }); + } else { + enrichedChanges.push(change); + } + } + + return NextResponse.json({ changes: enrichedChanges }, { status: 200 }); + } catch (error) { + console.error(`Failed to show contribution changes ${branchName}:`, error); + return NextResponse.json( + { + error: `Failed to show contribution changes for ${branchName}` + }, + { status: 500 } + ); + } finally { + // Ensure switching back to 'main' branch after any operation + try { + await git.checkout({ fs, dir: localTaxonomyDir, ref: 'main' }); + } catch (checkoutError) { + console.error('Failed to switch back to main branch:', checkoutError); + } + } +} + +async function findDiff(branchName: string, localTaxonomyDir: string): Promise { + // Fetch the commit SHA for `main` and the target branch + const mainCommit = await git.resolveRef({ fs, dir: localTaxonomyDir, ref: 'main' }); + const branchCommit = await git.resolveRef({ fs, dir: localTaxonomyDir, ref: branchName }); + + const mainFiles = await getFilesFromTree(mainCommit, localTaxonomyDir); + const branchFiles = await getFilesFromTree(branchCommit, localTaxonomyDir); + + // Create an array of Diffs to store changes + const changes: Diffs[] = []; + // Identify modified and deleted files + for (const file in mainFiles) { + if (branchFiles[file]) { + if (mainFiles[file] !== branchFiles[file]) { + changes.push({ file, status: 'modified' }); + } + } else { + changes.push({ file, status: 'deleted' }); + } + } + + // Identify added files + for (const file in branchFiles) { + if (!mainFiles[file]) { + changes.push({ file, status: 'added' }); + } + } + return changes; +} + +async function getTopCommitDetails(dir: string, ref: string = 'HEAD') { + try { + // Fetch the top commit (latest commit on the branch) + const [topCommit] = await git.log({ + fs, + dir, + ref, + depth: 1 // Only fetch the latest commit + }); + + if (!topCommit) { + throw new Error('No commits found in the repository.'); + } + + // Extract commit message + const commitMessage = topCommit.commit.message; + + // Check for Signed-off-by line + const signoffMatch = commitMessage.match(/^Signed-off-by: (.+)$/m); + const signoff = signoffMatch ? signoffMatch[1] : null; + + return { + message: commitMessage, + signoff + }; + } catch (error) { + console.error('Error reading top commit details:', error); + throw error; + } +} + +async function handlePublish(branchName: string, localTaxonomyDir: string, remoteTaxonomyDir: string) { + try { + if (!branchName || branchName === 'main') { + return NextResponse.json({ error: 'Invalid contribution name for publish' }, { status: 400 }); + } + + console.log(`Publishing contribution from ${branchName} to remote taxonomy repo at ${REMOTE_TAXONOMY_ROOT_DIR}/taxonomy`); + const changes = await findDiff(branchName, localTaxonomyDir); + + // Check if there are any changes to publish, create a new branch at remoteTaxonomyDir and + // copy all the files listed in the changes array to the new branch and create a commit + if (changes.length > 0) { + await git.checkout({ fs, dir: localTaxonomyDir, ref: branchName }); + // Read the commit message of the top commit from the branch + const details = await getTopCommitDetails(localTaxonomyDir); + + // Check if the remote branch exists, if not create it + const remoteBranchName = branchName; + const remoteBranchExists = await git.listBranches({ fs, dir: remoteTaxonomyDir }); + if (remoteBranchExists.includes(remoteBranchName)) { + console.log(`Branch ${remoteBranchName} exists in remote taxonomy, deleting it.`); + await git.deleteBranch({ fs, dir: remoteTaxonomyDir, ref: remoteBranchName }); + } else { + console.log(`Branch ${remoteBranchName} does not exist in remote taxonomy, creating a new branch.`); + } + + await git.checkout({ fs, dir: remoteTaxonomyDir, ref: 'main' }); + await git.branch({ fs, dir: remoteTaxonomyDir, ref: remoteBranchName }); + await git.checkout({ fs, dir: remoteTaxonomyDir, ref: remoteBranchName }); + + // Copy the files listed in the changes array to the remote branch and if the directories do not exist, create them + for (const change of changes) { + if (change.status !== 'deleted') { + const filePath = path.join(localTaxonomyDir, change.file); + const remoteFilePath = path.join(remoteTaxonomyDir, change.file); + const remoteFileDir = path.dirname(remoteFilePath); + if (!fs.existsSync(remoteFileDir)) { + fs.mkdirSync(remoteFileDir, { recursive: true }); + } + fs.copyFileSync(filePath, remoteFilePath); + } else { + // If deleted, ensure the file is removed from remote as well, if it exists + const remoteFilePath = path.join(remoteTaxonomyDir, change.file); + if (fs.existsSync(remoteFilePath)) { + fs.rmSync(remoteFilePath); + } + } + } + + await git.add({ fs, dir: remoteTaxonomyDir, filepath: '.' }); + + const authorInfo = details.signoff!.match(/(.*?) <(.*?)>/); + let authorName = ''; + let authorEmail = ''; + if (authorInfo) { + console.log(`Author information found in signoff: ${authorInfo}`); + authorName = authorInfo[1]; + authorEmail = authorInfo[2]; + } else { + return NextResponse.json({ message: `Author information is not present in the contribution ${branchName}.` }, { status: 500 }); + } + // Create a commit with the same message and signoff as the top commit from the local branch + await git.commit({ + fs, + dir: remoteTaxonomyDir, + message: details.message, + author: { + name: authorName, + email: authorEmail + } + }); + console.log(`Successfully published contribution ${branchName} to remote taxonomy repo at ${REMOTE_TAXONOMY_ROOT_DIR}/taxonomy.`); + return NextResponse.json( + { message: `Successfully published contribution ${branchName} to ${REMOTE_TAXONOMY_ROOT_DIR}/taxonomy.` }, + { status: 200 } + ); + } else { + return NextResponse.json({ message: `No changes to publish from contribution ${branchName}.` }, { status: 200 }); + } + } catch (error) { + console.error(`Failed to publish contribution from ${branchName}:`, error); + return NextResponse.json( + { + error: `Failed to publish contribution from ${branchName}` + }, + { status: 500 } + ); + } finally { + // Ensure switching back to 'main' branch after any operation + try { + await git.checkout({ fs, dir: localTaxonomyDir, ref: 'main' }); + } catch (checkoutError) { + console.error('Failed to switch back to main branch in local taxonomy repo:', checkoutError); + } + try { + await git.checkout({ fs, dir: remoteTaxonomyDir, ref: 'main' }); + } catch (checkoutError) { + console.error('Failed to switch back to main branch in remote taxonomy repo:', checkoutError); + } + } +} + +async function readFileFromBranch(localTaxonomyDir: string, branchName: string, filePath: string): Promise { + const tempDir = path.join(localTaxonomyDir, '.temp_checkout'); + if (!fs.existsSync(tempDir)) { + fs.mkdirSync(tempDir); + } + + const branchCommit = await git.resolveRef({ fs, dir: localTaxonomyDir, ref: branchName }); + const { blob } = await git.readBlob({ fs, dir: localTaxonomyDir, oid: branchCommit, filepath: filePath }); + + const decoder = new TextDecoder('utf-8'); + const content = decoder.decode(blob); + return content; +} + +async function getFilesFromTree(commitOid: string, repoDir: string) { + const fileMap: Record = {}; + + async function walkTree(dir: string) { + const tree = await git.readTree({ fs, dir: repoDir, oid: commitOid, filepath: dir }); + for (const entry of tree.tree) { + const fullPath = path.join(dir, entry.path); + if (entry.type === 'blob') { + fileMap[fullPath] = entry.oid; + } else if (entry.type === 'tree') { + await walkTree(fullPath); // Recursively walk subdirectories + } + } + } + + await walkTree(''); + return fileMap; +} diff --git a/src/app/api/native/git/knowledge-files/route.ts b/src/app/api/native/git/knowledge-files/route.ts new file mode 100644 index 00000000..1bfc47a8 --- /dev/null +++ b/src/app/api/native/git/knowledge-files/route.ts @@ -0,0 +1,212 @@ +// src/app/api/native/git/knowledge-files/route.ts + +'use server'; +import { NextRequest, NextResponse } from 'next/server'; +import * as git from 'isomorphic-git'; +import fs from 'fs'; +import path from 'path'; + +// Constants for repository paths +const LOCAL_TAXONOMY_DOCS_ROOT_DIR = + process.env.NEXT_PUBLIC_LOCAL_TAXONOMY_DOCS_ROOT_DIR || `${process.env.HOME}/.instructlab-ui/taxonomy-knowledge-docs`; + +// Interface for the response +interface KnowledgeFile { + filename: string; + content: string; + commitSha: string; + commitDate: string; +} + +interface Branch { + name: string; + commitSha: string; + commitDate: string; +} + +/** + * Function to list all branches. + */ +const listAllBranches = async (): Promise => { + const REPO_DIR = LOCAL_TAXONOMY_DOCS_ROOT_DIR; + + if (!fs.existsSync(REPO_DIR)) { + throw new Error('Repository path does not exist.'); + } + + const branches = await git.listBranches({ fs, dir: REPO_DIR }); + + const branchDetails: Branch[] = []; + + for (const branch of branches) { + try { + const latestCommit = await git.log({ fs, dir: REPO_DIR, ref: branch, depth: 1 }); + if (latestCommit.length === 0) { + continue; // No commits on this branch + } + + const commit = latestCommit[0]; + const commitSha = commit.oid; + const commitDate = new Date(commit.commit.committer.timestamp * 1000).toISOString(); + + branchDetails.push({ + name: branch, + commitSha: commitSha, + commitDate: commitDate + }); + } catch (error) { + console.error(`Failed to retrieve commit for branch ${branch}:`, error); + continue; + } + } + + return branchDetails; +}; + +/** + * Function to retrieve knowledge files from a specific branch. + * @param branchName - The name of the branch to retrieve files from. + * @returns An array of KnowledgeFile objects. + */ +const getKnowledgeFiles = async (branchName: string): Promise => { + const REPO_DIR = path.join(LOCAL_TAXONOMY_DOCS_ROOT_DIR, '/taxonomy-knowledge-docs'); + + // Ensure the repository path exists + if (!fs.existsSync(REPO_DIR)) { + throw new Error('Repository path does not exist.'); + } + + // Check if the branch exists + const branches = await git.listBranches({ fs, dir: REPO_DIR }); + if (!branches.includes(branchName)) { + throw new Error(`Branch "${branchName}" does not exist.`); + } + + // Checkout the specified branch + await git.checkout({ fs, dir: REPO_DIR, ref: branchName }); + + // Read all files in the repository root directory + const allFiles = fs.readdirSync(REPO_DIR); + + // Filter for Markdown files only + const markdownFiles = allFiles.filter((file) => path.extname(file).toLowerCase() === '.md'); + + const knowledgeFiles: KnowledgeFile[] = []; + + for (const file of markdownFiles) { + const filePath = path.join(REPO_DIR, file); + + // Check if the file is a regular file + const stat = fs.statSync(filePath); + if (!stat.isFile()) { + continue; + } + + try { + // Retrieve the latest commit SHA for the file on the specified branch + const logs = await git.log({ + fs, + dir: REPO_DIR, + ref: branchName, + filepath: file, + depth: 1 // Only the latest commit + }); + + if (logs.length === 0) { + // No commits found for this file; skip it + continue; + } + + const latestCommit = logs[0]; + const commitSha = latestCommit.oid; + const commitDate = new Date(latestCommit.commit.committer.timestamp * 1000).toISOString(); + + // Read the file content + const fileContent = fs.readFileSync(filePath, 'utf-8'); + + knowledgeFiles.push({ + filename: file, + content: fileContent, + commitSha: commitSha, + commitDate: commitDate + }); + } catch (error) { + console.error(`Failed to retrieve commit for file ${file}:`, error); + // Skip files that cause errors + continue; + } + } + + return knowledgeFiles; +}; + +/** + * Handler for GET requests. + * - If 'action=list-branches' is present, return all branches. + * - Else, return knowledge files from the 'main' branch. + */ +const getKnowledgeFilesHandler = async (req: NextRequest): Promise => { + try { + const { searchParams } = new URL(req.url); + const action = searchParams.get('action'); + + if (action === 'list-branches') { + const branches = await listAllBranches(); + return NextResponse.json({ branches }, { status: 200 }); + } + + // Default behavior: fetch files from 'main' branch + const branchName = 'main'; + const knowledgeFiles = await getKnowledgeFiles(branchName); + return NextResponse.json({ files: knowledgeFiles }, { status: 200 }); + } catch (error) { + console.error('Failed to retrieve knowledge files:', error); + return NextResponse.json({ error: (error as Error).message }, { status: 500 }); + } +}; + +/** + * Handler for POST requests. + * - If 'branchName' is provided, fetch files for that branch. + * - If 'action=diff', fetch files from the 'main' branch. + * - Else, return an error. + */ +const postKnowledgeFilesHandler = async (req: NextRequest): Promise => { + try { + const body = await req.json(); + const { action, branchName } = body; + + if (action === 'diff') { + // fetch files from main + const branchNameForDiff = 'main'; + const knowledgeFiles = await getKnowledgeFiles(branchNameForDiff); + return NextResponse.json({ files: knowledgeFiles }, { status: 200 }); + } + + if (branchName && typeof branchName === 'string') { + // Fetch files from a specified branch + const knowledgeFiles = await getKnowledgeFiles(branchName); + return NextResponse.json({ files: knowledgeFiles }, { status: 200 }); + } + + // If no valid action or branchName is provided + return NextResponse.json({ error: 'Invalid request. Provide an action or branchName.' }, { status: 400 }); + } catch (error) { + console.error('Failed to process POST request:', error); + return NextResponse.json({ error: (error as Error).message }, { status: 500 }); + } +}; + +/** + * GET handler to retrieve knowledge files or list branches based on 'action' query parameter. + */ +export async function GET(req: NextRequest) { + return await getKnowledgeFilesHandler(req); +} + +/** + * POST handler to retrieve knowledge files based on 'branchName' or 'action'. + */ +export async function POST(req: NextRequest) { + return await postKnowledgeFilesHandler(req); +} diff --git a/src/app/api/local/pr/knowledge/route.ts b/src/app/api/native/pr/knowledge/route.ts similarity index 78% rename from src/app/api/local/pr/knowledge/route.ts rename to src/app/api/native/pr/knowledge/route.ts index 353a04c4..155e5f4c 100644 --- a/src/app/api/local/pr/knowledge/route.ts +++ b/src/app/api/native/pr/knowledge/route.ts @@ -1,4 +1,4 @@ -// src/app/api/local/pr/knowledge/route.ts +// src/app/api/native/pr/knowledge/route.ts import { NextResponse } from 'next/server'; import { NextRequest } from 'next/server'; @@ -10,10 +10,12 @@ import { KnowledgeYamlData } from '@/types'; import yaml from 'js-yaml'; // Define paths and configuration -const REPO_DIR = process.env.NEXT_PUBLIC_LOCAL_REPO_PATH || '/path/to/local/repo'; // Update with actual local path +const LOCAL_TAXONOMY_ROOT_DIR = process.env.NEXT_PUBLIC_LOCAL_TAXONOMY_ROOT_DIR || `${process.env.HOME}/.instructlab-ui`; + const KNOWLEDGE_DIR = 'knowledge'; export async function POST(req: NextRequest) { + const REPO_DIR = path.join(LOCAL_TAXONOMY_ROOT_DIR, '/taxonomy'); try { // Extract the data from the request body const { content, attribution, name, email, submissionSummary, filePath } = await req.json(); @@ -69,9 +71,10 @@ Creator names: ${attribution.creator_names} }); // Respond with success message and branch name - return NextResponse.json({ message: 'Branch and commit created locally', branch: branchName }, { status: 201 }); + console.log(`Knowledge contribution submitted successfully to local taxonomy repo. Submission Name is ${branchName}.`); + return NextResponse.json({ message: 'Knowledge contribution submitted successfully.', branch: branchName }, { status: 201 }); } catch (error) { - console.error('Failed to create local branch and commit:', error); - return NextResponse.json({ error: 'Failed to create local branch and commit' }, { status: 500 }); + console.error(`Failed to submit knowledge contribution to local taxonomy repo:`, error); + return NextResponse.json({ error: 'Failed to submit knowledge contribution.' }, { status: 500 }); } } diff --git a/src/app/api/local/pr/skill/route.ts b/src/app/api/native/pr/skill/route.ts similarity index 67% rename from src/app/api/local/pr/skill/route.ts rename to src/app/api/native/pr/skill/route.ts index 873da074..cdfaefa0 100644 --- a/src/app/api/local/pr/skill/route.ts +++ b/src/app/api/native/pr/skill/route.ts @@ -1,16 +1,20 @@ -// src/app/api/local/pr/skill/route.ts +// src/app/api/native/pr/skill/route.ts import { NextResponse } from 'next/server'; import { NextRequest } from 'next/server'; import * as git from 'isomorphic-git'; import fs from 'fs'; import path from 'path'; import yaml from 'js-yaml'; +import { AttributionData, SkillYamlData } from '@/types'; +import { dumpYaml } from '@/utils/yamlConfig'; // Define paths and configuration -const REPO_DIR = process.env.NEXT_PUBLIC_LOCAL_REPO_PATH || '/path/to/local/repo'; // Update with actual local path +const LOCAL_TAXONOMY_ROOT_DIR = process.env.NEXT_PUBLIC_LOCAL_TAXONOMY_ROOT_DIR || `${process.env.HOME}/.instructlab-ui`; + const SKILLS_DIR = 'compositional_skills'; export async function POST(req: NextRequest) { + const REPO_DIR = path.join(LOCAL_TAXONOMY_ROOT_DIR, '/taxonomy'); try { // Extract the QnA data from the request body TODO: what is documentOutline? const { content, attribution, name, email, submissionSummary, documentOutline, filePath } = await req.json(); // eslint-disable-line @typescript-eslint/no-unused-vars @@ -20,12 +24,14 @@ export async function POST(req: NextRequest) { const newYamlFilePath = path.join(SKILLS_DIR, filePath, 'qna.yaml'); const newAttributionFilePath = path.join(SKILLS_DIR, filePath, 'attribution.txt'); - // Prepare file content - const yamlString = yaml.dump(content); + const skillData = yaml.load(content) as SkillYamlData; + const attributionData = attribution as AttributionData; + + const yamlString = dumpYaml(skillData); const attributionString = ` -Title of work: ${attribution.title_of_work} -License of the work: ${attribution.license_of_the_work} -Creator names: ${attribution.creator_names} +Title of work: ${attributionData.title_of_work} +License of the work: ${attributionData.license_of_the_work} +Creator names: ${attributionData.creator_names} `; // Initialize the repository if it doesn’t exist @@ -62,9 +68,10 @@ Creator names: ${attribution.creator_names} }); // Respond with success - return NextResponse.json({ message: 'Branch and commit created locally', branch: branchName }, { status: 201 }); + console.log('Skill contribution submitted successfully. Submission name is ', branchName); + return NextResponse.json({ message: 'Skill contribution submitted successfully.', branch: branchName }, { status: 201 }); } catch (error) { console.error('Failed to create local branch and commit:', error); - return NextResponse.json({ error: 'Failed to create local branch and commit' }, { status: 500 }); + return NextResponse.json({ error: 'Failed to submit skill contribution.' }, { status: 500 }); } } diff --git a/src/app/api/native/upload/route.ts b/src/app/api/native/upload/route.ts new file mode 100644 index 00000000..b67a452f --- /dev/null +++ b/src/app/api/native/upload/route.ts @@ -0,0 +1,98 @@ +// src/app/api/native/upload/route.ts +import { NextResponse } from 'next/server'; +import { NextRequest } from 'next/server'; +import * as git from 'isomorphic-git'; +import http from 'isomorphic-git/http/node'; +import path from 'path'; +import fs from 'fs'; + +const LOCAL_TAXONOMY_DOCS_ROOT_DIR = process.env.NEXT_PUBLIC_LOCAL_TAXONOMY_ROOT_DIR || `${process.env.HOME}/.instructlab-ui`; +const TAXONOMY_KNOWLEDGE_DOCS_REPO_URL = 'https://github.com/instructlab-public/taxonomy-knowledge-docs.git'; + +export async function POST(req: NextRequest) { + try { + const body = await req.json(); + const { files } = body; + const docsRepoUrl = await cloneTaxonomyDocsRepo(); + + // If the repository was not cloned, return an error + if (!docsRepoUrl) { + return NextResponse.json({ error: 'Failed to clone taxonomy knowledge docs repository' }, { status: 500 }); + } + + const timestamp = new Date().toISOString().replace(/[-:.]/g, '').replace('T', 'T').slice(0, -1); + const filesWithTimestamp = files.map((file: { fileName: string; fileContent: string }) => { + const [name, extension] = file.fileName.split(/\.(?=[^.]+$)/); + return { + fileName: `${name}-${timestamp}.${extension}`, + fileContent: file.fileContent + }; + }); + + // Write the files to the repository + for (const file of filesWithTimestamp) { + const filePath = path.join(docsRepoUrl, file.fileName); + fs.writeFileSync(filePath, file.fileContent); + } + + // Checkout the main branch + await git.checkout({ fs, dir: docsRepoUrl, ref: 'main' }); + + // Stage the files + await git.add({ fs, dir: docsRepoUrl, filepath: '.' }); + + // Commit the files + const commitSha = await git.commit({ + fs, + dir: docsRepoUrl, + author: { name: 'instructlab-ui', email: 'ui@instructlab.ai' }, + message: `Add files: ${files + .map((file: { fileName: string; fileContent: string }) => file.fileName) + .join(', ')}\n\nSigned-off-by: ui@instructlab.ai` + }); + + return NextResponse.json( + { + repoUrl: docsRepoUrl, + commitSha, + documentNames: filesWithTimestamp.map((file: { fileName: string }) => file.fileName), + prUrl: '' + }, + { status: 201 } + ); + } catch (error) { + console.error('Failed to upload documents:', error); + return NextResponse.json({ error: 'Failed to upload documents' }, { status: 500 }); + } +} + +async function cloneTaxonomyDocsRepo() { + const taxonomyDocsDirectoryPath = path.join(LOCAL_TAXONOMY_DOCS_ROOT_DIR, '/taxonomy-knowledge-docs'); + console.log(`Cloning taxonomy docs repository to ${taxonomyDocsDirectoryPath}...`); + + if (fs.existsSync(taxonomyDocsDirectoryPath)) { + console.log(`Using existing taxonomy knowledge docs repository at ${taxonomyDocsDirectoryPath}.`); + return taxonomyDocsDirectoryPath; + } else { + console.log(`Taxonomy knowledge docs repository not found at ${taxonomyDocsDirectoryPath}. Cloning...`); + } + + try { + await git.clone({ + fs, + http, + dir: taxonomyDocsDirectoryPath, + url: TAXONOMY_KNOWLEDGE_DOCS_REPO_URL, + singleBranch: true, + depth: 1 + }); + + // Include the full path in the response for client display + console.log(`Repository cloned successfully to ${taxonomyDocsDirectoryPath}.`); + return taxonomyDocsDirectoryPath; + } catch (error: unknown) { + const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; + console.error(`Failed to clone taxonomy docs repository: ${errorMessage}`); + return null; + } +} diff --git a/src/app/api/tree/route.ts b/src/app/api/tree/route.ts index a3d14077..7af32894 100644 --- a/src/app/api/tree/route.ts +++ b/src/app/api/tree/route.ts @@ -2,18 +2,14 @@ import axios from 'axios'; import { NextRequest, NextResponse } from 'next/server'; -const DEPLOYMENT = process.env.IL_UI_DEPLOYMENT!; -const EXPERIMENTAL_FEATURES = process.env.NEXT_PUBLIC_EXPERIMENTAL_FEATURES || ''; +const PATH_SERVICE_URL = process.env.IL_PATH_SERVICE_URL || 'http://pathservice:4000/tree/'; export async function POST(req: NextRequest) { const body = await req.json(); const { root_path, dir_name } = body; try { - let apiBaseUrl = 'http://pathservice:4000/tree/'; - if (DEPLOYMENT === 'dev' && EXPERIMENTAL_FEATURES !== 'true') { - apiBaseUrl = 'http://localhost:4000/tree/'; - } + const apiBaseUrl = PATH_SERVICE_URL; const response = await axios.get(apiBaseUrl + root_path, { params: { dir_name: dir_name } }); diff --git a/src/app/api/upload/route.ts b/src/app/api/upload/route.ts index b1e2dc78..83297637 100644 --- a/src/app/api/upload/route.ts +++ b/src/app/api/upload/route.ts @@ -4,7 +4,7 @@ import { getToken } from 'next-auth/jwt'; import { NextRequest } from 'next/server'; const GITHUB_API_URL = 'https://api.github.com'; -const TAXONOMY_DOCUMENTS_REPO = process.env.TAXONOMY_DOCUMENTS_REPO!; +const TAXONOMY_DOCUMENTS_REPO = process.env.NEXT_PUBLIC_TAXONOMY_DOCUMENTS_REPO!; const BASE_BRANCH = 'main'; export async function POST(req: NextRequest) { diff --git a/src/app/contribute/knowledge/page.tsx b/src/app/contribute/knowledge/page.tsx index cab86364..9caebe86 100644 --- a/src/app/contribute/knowledge/page.tsx +++ b/src/app/contribute/knowledge/page.tsx @@ -1,14 +1,23 @@ // src/app/contribute/knowledge/page.tsx -import * as React from 'react'; -import { AppLayout } from '../../../components/AppLayout'; -import { KnowledgeForm } from '../../../components/Contribute/Knowledge'; - -const KnowledgeFormPage: React.FC = () => { - return ( - - - - ); +'use client'; +import { AppLayout } from '@/components/AppLayout'; +import { KnowledgeFormGithub } from '@/components/Contribute/Knowledge/Github/index'; +import KnowledgeFormNative from '@/components/Contribute/Knowledge/Native/index'; +import { useEffect, useState } from 'react'; + +const KnowledgeFormPage: React.FunctionComponent = () => { + const [deploymentType, setDeploymentType] = useState(); + + useEffect(() => { + const getEnvVariables = async () => { + const res = await fetch('/api/envConfig'); + const envConfig = await res.json(); + setDeploymentType(envConfig.DEPLOYMENT_TYPE); + }; + getEnvVariables(); + }, []); + + return {deploymentType === 'native' ? : }; }; export default KnowledgeFormPage; diff --git a/src/app/contribute/skill/page.tsx b/src/app/contribute/skill/page.tsx index a2b5d3bc..b4d31d35 100644 --- a/src/app/contribute/skill/page.tsx +++ b/src/app/contribute/skill/page.tsx @@ -1,14 +1,23 @@ // src/app/contribute/skill/page.tsx -import * as React from 'react'; -import { AppLayout } from '../../../components/AppLayout'; -import { SkillForm } from '../../../components/Contribute/Skill'; - -const SkillFormPage: React.FC = () => { - return ( - - - - ); +'use client'; +import { AppLayout } from '@/components/AppLayout'; +import { SkillFormGithub } from '@/components/Contribute/Skill/Github/index'; +import { SkillFormNative } from '@/components/Contribute/Skill/Native/index'; +import { useEffect, useState } from 'react'; + +const SkillFormPage: React.FunctionComponent = () => { + const [deploymentType, setDeploymentType] = useState(); + + useEffect(() => { + const getEnvVariables = async () => { + const res = await fetch('/api/envConfig'); + const envConfig = await res.json(); + setDeploymentType(envConfig.DEPLOYMENT_TYPE); + }; + getEnvVariables(); + }, []); + + return {deploymentType === 'native' ? : }; }; export default SkillFormPage; diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 646f1dfd..ee28b27a 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -1,17 +1,25 @@ -// src/app/page.tsx +// src/app/dashboard/page.tsx 'use client'; -import * as React from 'react'; import '@patternfly/react-core/dist/styles/base.css'; import { AppLayout } from '@/components/AppLayout'; -import { Index } from '@/components/Dashboard'; +import { DashboardGithub } from '@/components/Dashboard/Github/dashboard'; +import { DashboardNative } from '@/components/Dashboard/Native/dashboard'; +import { useEffect, useState } from 'react'; const Home: React.FunctionComponent = () => { - return ( - - - - ); + const [deploymentType, setDeploymentType] = useState(); + + useEffect(() => { + const getEnvVariables = async () => { + const res = await fetch('/api/envConfig'); + const envConfig = await res.json(); + setDeploymentType(envConfig.DEPLOYMENT_TYPE); + }; + getEnvVariables(); + }, []); + + return {deploymentType === 'native' ? : }; }; export default Home; diff --git a/src/app/experimental/contribute-local/configuration-local/page.tsx b/src/app/experimental/contribute-local/configuration-local/page.tsx deleted file mode 100644 index c7550df8..00000000 --- a/src/app/experimental/contribute-local/configuration-local/page.tsx +++ /dev/null @@ -1,14 +0,0 @@ -// src/app/experimental/contribute-local/clone-repo/page.tsx -import * as React from 'react'; -import { AppLayout } from '@/components/AppLayout'; -import CloneRepoLocal from '@/components/Experimental/CloneRepoLocal/CloneRepoLocal'; - -const CloneRepoPage: React.FC = () => { - return ( - - - - ); -}; - -export default CloneRepoPage; diff --git a/src/app/experimental/contribute-local/knowledge/page.tsx b/src/app/experimental/contribute-local/knowledge/page.tsx deleted file mode 100644 index dc1a4cf2..00000000 --- a/src/app/experimental/contribute-local/knowledge/page.tsx +++ /dev/null @@ -1,14 +0,0 @@ -// src/app/experimental/contribute-local/knowledge/page.tsx -import * as React from 'react'; -import { AppLayout } from '@/components/AppLayout'; -import { KnowledgeFormLocal } from '@/components/Experimental/ContributeLocal/Knowledge'; - -const KnowledgeFormLocalPage: React.FC = () => { - return ( - - - - ); -}; - -export default KnowledgeFormLocalPage; diff --git a/src/app/experimental/contribute-local/skill/page.tsx b/src/app/experimental/contribute-local/skill/page.tsx deleted file mode 100644 index 9bcdc347..00000000 --- a/src/app/experimental/contribute-local/skill/page.tsx +++ /dev/null @@ -1,14 +0,0 @@ -// src/app/experimental/contribute-local/skill/page.tsx -import * as React from 'react'; -import { AppLayout } from '@/components/AppLayout'; -import SkillFormLocal from '@/components/Experimental/ContributeLocal/Skill'; - -const SkillFormPageLocal: React.FC = () => { - return ( - - - - ); -}; - -export default SkillFormPageLocal; diff --git a/src/app/experimental/dashboard-local/page.tsx b/src/app/experimental/dashboard-local/page.tsx deleted file mode 100644 index e1da680f..00000000 --- a/src/app/experimental/dashboard-local/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -// src/app/experimental/dashboard-local/page.tsx -'use client'; - -import * as React from 'react'; -import '@patternfly/react-core/dist/styles/base.css'; -import { AppLayout } from '@/components/AppLayout'; -import { DashboardLocal } from '@/components/Experimental/DashboardLocal'; - -const Home: React.FunctionComponent = () => { - return ( - - - - ); -}; - -export default Home; diff --git a/src/app/login/locallogin.tsx b/src/app/login/devmodelogin.tsx similarity index 96% rename from src/app/login/locallogin.tsx rename to src/app/login/devmodelogin.tsx index f30e1136..21df3eb0 100644 --- a/src/app/login/locallogin.tsx +++ b/src/app/login/devmodelogin.tsx @@ -1,4 +1,4 @@ -// src/app/login/LocalLogin.tsx +// src/app/login/DevModeLogin.tsx import React, { useState } from 'react'; import { signIn } from 'next-auth/react'; import { Grid, GridItem } from '@patternfly/react-core/dist/dynamic/layouts/Grid'; @@ -12,7 +12,7 @@ import { HelperTextItem } from '@patternfly/react-core/dist/dynamic/components/H import GithubIcon from '@patternfly/react-icons/dist/dynamic/icons/github-icon'; import './githublogin.css'; -const LocalLogin: React.FunctionComponent = () => { +const DevModeLogin: React.FunctionComponent = () => { const [, setShowHelperText] = useState(false); const [username, setUsername] = useState(''); const [isValidUsername, setIsValidUsername] = useState(true); @@ -27,7 +27,7 @@ const LocalLogin: React.FunctionComponent = () => { setIsValidUsername(false); setIsValidPassword(false); } else { - window.location.href = '/'; + window.location.href = '/dashboard'; } }; @@ -40,7 +40,7 @@ const LocalLogin: React.FunctionComponent = () => { }; const handleGitHubLogin = () => { - signIn('github', { callbackUrl: '/' }); + signIn('github', { callbackUrl: '/dashboard' }); }; return ( @@ -158,4 +158,4 @@ const LocalLogin: React.FunctionComponent = () => { ); }; -export default LocalLogin; +export default DevModeLogin; diff --git a/src/app/login/githublogin.tsx b/src/app/login/githublogin.tsx index 4b20c9b8..8281b487 100644 --- a/src/app/login/githublogin.tsx +++ b/src/app/login/githublogin.tsx @@ -11,8 +11,8 @@ import { useRouter, useSearchParams } from 'next/navigation'; import { Modal, ModalVariant } from '@patternfly/react-core/dist/esm/deprecated/components/Modal'; const GithubLogin: React.FC = () => { - const searchParams = useSearchParams(); const router = useRouter(); + const searchParams = useSearchParams(); const [showError, setShowError] = useState(false); const [errorMsg, setErrorMsg] = useState('Something went wrong.'); const [githubUsername, setGithubUsername] = useState(null); diff --git a/src/app/login/nativelogin.tsx b/src/app/login/nativelogin.tsx new file mode 100644 index 00000000..5f0c76e7 --- /dev/null +++ b/src/app/login/nativelogin.tsx @@ -0,0 +1,146 @@ +// src/app/login/NativeLogin.tsx +import React, { useState } from 'react'; +import { signIn } from 'next-auth/react'; +import { Grid, GridItem } from '@patternfly/react-core/dist/dynamic/layouts/Grid'; +import { Content } from '@patternfly/react-core/dist/dynamic/components/Content'; +import { Form } from '@patternfly/react-core/dist/dynamic/components/Form'; +import { FormGroup } from '@patternfly/react-core/dist/dynamic/components/Form'; +import { TextInput } from '@patternfly/react-core/dist/dynamic/components/TextInput'; +import { Button } from '@patternfly/react-core/dist/dynamic/components/Button'; +import { HelperText } from '@patternfly/react-core/dist/dynamic/components/HelperText'; +import { HelperTextItem } from '@patternfly/react-core/dist/dynamic/components/HelperText'; +import './githublogin.css'; + +const NativeLogin: React.FunctionComponent = () => { + const [, setShowHelperText] = useState(false); + const [username, setUsername] = useState(''); + const [isValidUsername, setIsValidUsername] = useState(true); + const [password, setPassword] = useState(''); + const [isValidPassword, setIsValidPassword] = useState(true); + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + const result = await signIn('credentials', { redirect: false, username, password }); + if (result?.error) { + setShowHelperText(true); + setIsValidUsername(false); + setIsValidPassword(false); + } else { + window.location.href = '/dashboard'; + } + }; + + const handleUsernameChange = (_event: React.FormEvent, value: string) => { + setUsername(value); + }; + + const handlePasswordChange = (_event: React.FormEvent, value: string) => { + setPassword(value); + }; + + return ( +
+ + + + + Login locally with a username and password or via GitHub OAuth + + + + + Join the novel, community-based movement to create truly open-source LLMs + + +
+
+ + + {!isValidUsername && ( + + Invalid Username + + )} + + + + {!isValidPassword && ( + + Invalid password + + )} + + +
+
+ + + + GitHub + {' '} + |{' '} + + Collaborate + {' '} + |{' '} + + Code Of Conduct + + + + + Terms of use + {' '} + |{' '} + + Privacy Policy + + + +
+
+
+ ); +}; + +export default NativeLogin; diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index cf5e59ce..85feca05 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,34 +1,43 @@ // src/app/login/page.tsx 'use client'; -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, Suspense } from 'react'; import './githublogin.css'; -import LocalLogin from '@/app/login/locallogin'; +import NativeLogin from '@/app/login/nativelogin'; import GithubLogin from '@/app/login/githublogin'; +import DevModeLogin from './devmodelogin'; const Login: React.FunctionComponent = () => { - const [isProd, setIsProd] = useState(null); + const [deploymentType, setDeploymentType] = useState(); + const [isDevModeEnabled, setIsDevModeEnabled] = useState(false); useEffect(() => { const chooseLoginPage = async () => { try { const res = await fetch('/api/envConfig'); const envConfig = await res.json(); - setIsProd(envConfig.DEPLOYMENT_TYPE !== 'dev'); + setDeploymentType(envConfig.DEPLOYMENT_TYPE); + setIsDevModeEnabled(envConfig.ENABLE_DEV_MODE === 'true'); } catch (error) { console.error('Error fetching environment config:', error); - setIsProd(true); + setDeploymentType('github'); } }; chooseLoginPage(); }, []); - if (isProd === null) { + if (isDevModeEnabled) { + return ; + } + if (deploymentType === 'native') { // Render a loading indicator or null while determining the environment - return null; + return ; } - - return isProd ? : ; + return ( + + + + ); }; export default Login; diff --git a/src/app/page.tsx b/src/app/page.tsx index 6853aa0b..75a82eda 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,11 +1,11 @@ // src/app/page.tsx 'use client'; +import { DashboardGithub } from '@/components/Dashboard/Github/dashboard'; import { GithubAccessPopup } from '@/components/GithubAccessPopup'; import * as React from 'react'; import { useState } from 'react'; import { AppLayout } from '../components/AppLayout'; -import { Index } from '../components/Dashboard'; const HomePage: React.FC = () => { const [isWarningConditionAccepted, setIsWarningConditionAccepted] = useState(false); @@ -19,7 +19,7 @@ const HomePage: React.FC = () => { return ( - {isWarningConditionAccepted && } + {isWarningConditionAccepted && } ); }; diff --git a/src/components/AppLayout.tsx b/src/components/AppLayout.tsx index fe059946..b0196c23 100644 --- a/src/components/AppLayout.tsx +++ b/src/components/AppLayout.tsx @@ -21,12 +21,13 @@ import { PageSidebar } from '@patternfly/react-core/dist/dynamic/components/Page import { PageSidebarBody } from '@patternfly/react-core/dist/dynamic/components/Page'; import { SkipToContent } from '@patternfly/react-core/dist/dynamic/components/SkipToContent'; import { Spinner } from '@patternfly/react-core/dist/dynamic/components/Spinner'; +import { Bullseye } from '@patternfly/react-core/dist/dynamic/layouts/Bullseye'; import UserMenu from './UserMenu/UserMenu'; import { useSession } from 'next-auth/react'; // import { useTheme } from '../context/ThemeContext'; import { useState } from 'react'; +import '@/components/styles/globals.scss'; import '@/components/app.scss'; - interface IAppLayout { children: React.ReactNode; } @@ -64,21 +65,17 @@ const AppLayout: React.FunctionComponent = ({ children }) => { }, [session, status, pathname, router]); if (status === 'loading') { - return ; + return ( + + + + ); } if (!session) { return null; // Return nothing if not authenticated to avoid flicker } - //const isExperimentalEnabled = process.env.NEXT_PUBLIC_EXPERIMENTAL_FEATURES === 'true'; - - // Only log if experimental features are enabled - if (isExperimentalEnabled) { - console.log('Is Experimental Enabled:', isExperimentalEnabled); - console.log('Environment Variable:', process.env.NEXT_PUBLIC_EXPERIMENTAL_FEATURES); - } - const routes = [ { path: '/dashboard', label: 'Dashboard' }, { @@ -101,10 +98,6 @@ const AppLayout: React.FunctionComponent = ({ children }) => { path: '/experimental', label: 'Experimental Features', children: [ - { path: '/experimental/dashboard-local/', label: 'Local Dashboard' }, - { path: '/experimental/contribute-local/skill/', label: 'Local Skill' }, - { path: '/experimental/contribute-local/knowledge/', label: 'Local Knowledge' }, - { path: '/experimental/contribute-local/configuration-local/', label: 'Local Configuration' }, { path: '/experimental/fine-tune/', label: 'Fine-tuning' }, { path: '/experimental/chat-eval/', label: 'Model Chat Eval' } ] diff --git a/src/components/Contribute/EditKnowledge/EditKnowledge.tsx b/src/components/Contribute/EditKnowledge/EditKnowledge.tsx index b37a35cf..3a8a2639 100644 --- a/src/components/Contribute/EditKnowledge/EditKnowledge.tsx +++ b/src/components/Contribute/EditKnowledge/EditKnowledge.tsx @@ -8,11 +8,12 @@ import { KnowledgeSchemaVersion } from '@/types/const'; import { fetchPullRequest, fetchFileContent, fetchPullRequestFiles } from '@/utils/github'; import yaml from 'js-yaml'; import axios from 'axios'; -import KnowledgeForm, { KnowledgeEditFormData, KnowledgeFormData, QuestionAndAnswerPair, SeedExample } from '@/components/Contribute/Knowledge'; +import { KnowledgeEditFormData, KnowledgeFormData, QuestionAndAnswerPair, KnowledgeSeedExample } from '@/types'; import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants'; import { useEffect, useState } from 'react'; import { Modal, ModalVariant } from '@patternfly/react-core/dist/esm/deprecated/components/Modal/Modal'; import { useRouter } from 'next/navigation'; +import KnowledgeFormGithub from '../Knowledge/Github'; interface EditKnowledgeClientComponentProps { prNumber: number; @@ -83,10 +84,10 @@ const EditKnowledge: React.FC = ({ prNumber } knowledgeExistingFormData.knowledgeDocumentCommit = yamlData.document.commit; knowledgeExistingFormData.documentName = yamlData.document.patterns.join(', '); - const seedExamples: SeedExample[] = []; + const seedExamples: KnowledgeSeedExample[] = []; yamlData.seed_examples.forEach((seed, index) => { // iterate through questions_and_answers and create a new object for each - const example: SeedExample = { + const example: KnowledgeSeedExample = { immutable: index < 5 ? true : false, isExpanded: true, context: seed.context, @@ -175,7 +176,7 @@ const EditKnowledge: React.FC = ({ prNumber } return ( // - + // ); }; diff --git a/src/components/Contribute/EditSkill/EditSkill.tsx b/src/components/Contribute/EditSkill/EditSkill.tsx index f98ae791..3212ae4f 100644 --- a/src/components/Contribute/EditSkill/EditSkill.tsx +++ b/src/components/Contribute/EditSkill/EditSkill.tsx @@ -7,11 +7,11 @@ import { useEffect, useState } from 'react'; import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants'; import { Modal, ModalVariant } from '@patternfly/react-core/dist/esm/deprecated/components/Modal/Modal'; import { useRouter } from 'next/navigation'; -import SkillForm, { SkillEditFormData, SkillFormData, SeedExample } from '@/components/Contribute/Skill'; +import SkillFormGithub, { SkillEditFormData } from '@/components/Contribute/Skill/Github'; import { fetchPullRequest, fetchFileContent, fetchPullRequestFiles } from '@/utils/github'; import yaml from 'js-yaml'; import axios from 'axios'; -import { SkillYamlData, AttributionData, PullRequestFile } from '@/types'; +import { SkillYamlData, AttributionData, PullRequestFile, SkillFormData, SkillSeedExample } from '@/types'; import { SkillSchemaVersion } from '@/types/const'; interface EditSkillClientComponentProps { @@ -72,9 +72,9 @@ const EditSkill: React.FC = ({ prNumber }) => { // Populate the form fields with YAML data skillExistingFormData.documentOutline = yamlData.task_description; - const seedExamples: SeedExample[] = []; + const seedExamples: SkillSeedExample[] = []; yamlData.seed_examples.forEach((seed, index) => { - const example: SeedExample = { + const example: SkillSeedExample = { immutable: index < 5 ? true : false, isExpanded: true, context: seed.context || '', @@ -147,7 +147,7 @@ const EditSkill: React.FC = ({ prNumber }) => { ); } - return ; + return ; }; export default EditSkill; diff --git a/src/components/Contribute/Knowledge/AttributionInformation/AttributionInformation.tsx b/src/components/Contribute/Knowledge/AttributionInformation/AttributionInformation.tsx index 87f97851..b606a3ce 100644 --- a/src/components/Contribute/Knowledge/AttributionInformation/AttributionInformation.tsx +++ b/src/components/Contribute/Knowledge/AttributionInformation/AttributionInformation.tsx @@ -5,8 +5,8 @@ import { HelperText } from '@patternfly/react-core/dist/dynamic/components/Helpe import { HelperTextItem } from '@patternfly/react-core/dist/dynamic/components/HelperText'; import ExclamationCircleIcon from '@patternfly/react-icons/dist/dynamic/icons/exclamation-circle-icon'; import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants'; -import { KnowledgeFormData } from '..'; import { checkKnowledgeFormCompletion } from '../validation'; +import { KnowledgeFormData } from '@/types'; interface Props { reset: boolean; diff --git a/src/components/Contribute/Knowledge/AutoFill.ts b/src/components/Contribute/Knowledge/AutoFill.ts index 568db39b..7e05911b 100644 --- a/src/components/Contribute/Knowledge/AutoFill.ts +++ b/src/components/Contribute/Knowledge/AutoFill.ts @@ -1,4 +1,4 @@ -import { KnowledgeFormData, QuestionAndAnswerPair, SeedExample } from '.'; +import { KnowledgeFormData, KnowledgeSeedExample, QuestionAndAnswerPair } from '@/types'; import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants'; const questionAndAnswerPairs1: QuestionAndAnswerPair[] = [ @@ -127,7 +127,7 @@ const questionAndAnswerPairs5: QuestionAndAnswerPair[] = [ } ]; -const seedExamples: SeedExample[] = [ +const seedExamples: KnowledgeSeedExample[] = [ { immutable: true, isExpanded: true, @@ -238,12 +238,12 @@ const seedExamples: SeedExample[] = [ { immutable: true, isExpanded: true, - context: `Phoenix is the radiant of two annual meteor showers. The Phoenicids, - also known as the December Phoenicids, were first observed on 3 December 1887. - The shower was particularly intense in December 1956, and is thought related - to the breakup of the short-period comet 289P/Blanpain. It peaks around 4–5 - December, though is not seen every year.[58] A very minor meteor shower peaks - around July 14 with around one meteor an hour, though meteors can be seen + context: `Phoenix is the radiant of two annual meteor showers. The Phoenicids, + also known as the December Phoenicids, were first observed on 3 December 1887. + The shower was particularly intense in December 1956, and is thought related + to the breakup of the short-period comet 289P/Blanpain. It peaks around 4–5 + December, though is not seen every year.[58] A very minor meteor shower peaks + around July 14 with around one meteor an hour, though meteors can be seen anytime from July 3 to 18; this shower is referred to as the July Phoenicids.[59]`, isContextValid: ValidatedOptions.success, questionAndAnswers: questionAndAnswerPairs5 diff --git a/src/components/Contribute/Knowledge/DownloadAttribution/DownloadAttribution.tsx b/src/components/Contribute/Knowledge/DownloadAttribution/DownloadAttribution.tsx index 3f0fe267..d61394fe 100644 --- a/src/components/Contribute/Knowledge/DownloadAttribution/DownloadAttribution.tsx +++ b/src/components/Contribute/Knowledge/DownloadAttribution/DownloadAttribution.tsx @@ -1,7 +1,8 @@ import React from 'react'; -import { KnowledgeFormData } from '..'; import { DropdownItem } from '@patternfly/react-core/dist/esm/components/Dropdown/DropdownItem'; +import { Icon } from '@patternfly/react-core/dist/dynamic/components/Icon'; import FileIcon from '@patternfly/react-icons/dist/esm/icons/file-icon'; +import { KnowledgeFormData } from '@/types'; interface Props { knowledgeFormData: KnowledgeFormData; @@ -29,8 +30,18 @@ const DownloadAttribution: React.FC = ({ knowledgeFormData }) => { }; return ( - - Attribution File + + + + } + > + {' '} + Attribution File ); }; diff --git a/src/components/Contribute/Knowledge/DownloadDropdown/DownloadDropdown.tsx b/src/components/Contribute/Knowledge/DownloadDropdown/DownloadDropdown.tsx index c7aa0dca..2dc99c3f 100644 --- a/src/components/Contribute/Knowledge/DownloadDropdown/DownloadDropdown.tsx +++ b/src/components/Contribute/Knowledge/DownloadDropdown/DownloadDropdown.tsx @@ -1,11 +1,12 @@ import React from 'react'; import { Dropdown } from '@patternfly/react-core/dist/dynamic/components/Dropdown'; import { DropdownList } from '@patternfly/react-core/dist/dynamic/components/Dropdown'; +import { Icon } from '@patternfly/react-core/dist/dynamic/components/Icon'; import { MenuToggle, MenuToggleElement } from '@patternfly/react-core/dist/dynamic/components/MenuToggle'; import DownloadYaml from '../DownloadYaml/DownloadYaml'; import DownloadAttribution from '../DownloadAttribution/DownloadAttribution'; -import { KnowledgeFormData } from '..'; import DownloadIcon from '@patternfly/react-icons/dist/esm/icons/download-icon'; +import { KnowledgeFormData } from '@/types'; interface Props { knowledgeFormData: KnowledgeFormData; @@ -30,8 +31,18 @@ export const DownloadDropdown: React.FunctionComponent = ({ knowledgeForm onSelect={onSelect} onOpenChange={(isOpen: boolean) => setIsOpen(isOpen)} toggle={(toggleRef: React.Ref) => ( - - Download + + {' '} + + } + > + {' '} + Download )} ouiaId="DownloadDropdown" diff --git a/src/components/Contribute/Knowledge/DownloadYaml/DownloadYaml.tsx b/src/components/Contribute/Knowledge/DownloadYaml/DownloadYaml.tsx index c5990b9e..f39b6c2a 100644 --- a/src/components/Contribute/Knowledge/DownloadYaml/DownloadYaml.tsx +++ b/src/components/Contribute/Knowledge/DownloadYaml/DownloadYaml.tsx @@ -1,8 +1,8 @@ import React from 'react'; -import { KnowledgeFormData } from '..'; -import { KnowledgeYamlData } from '@/types'; +import { KnowledgeFormData, KnowledgeYamlData } from '@/types'; import { KnowledgeSchemaVersion } from '@/types/const'; import { dumpYaml } from '@/utils/yamlConfig'; +import { Icon } from '@patternfly/react-core/dist/dynamic/components/Icon'; import { DropdownItem } from '@patternfly/react-core/dist/esm/components/Dropdown/DropdownItem'; import CodeIcon from '@patternfly/react-icons/dist/esm/icons/code-icon'; @@ -43,8 +43,18 @@ const DownloadYaml: React.FC = ({ knowledgeFormData, githubUsername }) => document.body.removeChild(a); }; return ( - - Yaml File + + + + } + > + {' '} + YAML File ); }; diff --git a/src/components/Contribute/Knowledge/DocumentInformation/DocumentInformation.tsx b/src/components/Contribute/Knowledge/Github/DocumentInformation/DocumentInformation.tsx similarity index 81% rename from src/components/Contribute/Knowledge/DocumentInformation/DocumentInformation.tsx rename to src/components/Contribute/Knowledge/Github/DocumentInformation/DocumentInformation.tsx index 53f249d3..786c441c 100644 --- a/src/components/Contribute/Knowledge/DocumentInformation/DocumentInformation.tsx +++ b/src/components/Contribute/Knowledge/Github/DocumentInformation/DocumentInformation.tsx @@ -2,15 +2,15 @@ import React, { useEffect, useState } from 'react'; import { FormFieldGroupHeader, FormGroup, FormHelperText } from '@patternfly/react-core/dist/dynamic/components/Form'; import { Button } from '@patternfly/react-core/dist/dynamic/components/Button'; import { TextInput } from '@patternfly/react-core/dist/dynamic/components/TextInput'; -import { UploadFile } from './../UploadFile'; -import { Alert, AlertActionLink, AlertActionCloseButton } from '@patternfly/react-core/dist/dynamic/components/Alert'; +import { UploadFile } from '../../UploadFile'; +import { Alert, AlertActionLink, AlertActionCloseButton, AlertGroup } from '@patternfly/react-core/dist/dynamic/components/Alert'; import { HelperText } from '@patternfly/react-core/dist/dynamic/components/HelperText'; import { HelperTextItem } from '@patternfly/react-core/dist/dynamic/components/HelperText'; import ExclamationCircleIcon from '@patternfly/react-icons/dist/dynamic/icons/exclamation-circle-icon'; import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants'; -import { KnowledgeFormData } from '..'; -import { checkKnowledgeFormCompletion } from '../validation'; +import { checkKnowledgeFormCompletion } from '../../validation'; import { Modal, ModalVariant } from '@patternfly/react-core/dist/esm/deprecated/components/Modal/Modal'; +import { KnowledgeFormData } from '@/types'; interface Props { reset: boolean; @@ -41,18 +41,18 @@ const DocumentInformation: React.FC = ({ const [uploadedFiles, setUploadedFiles] = useState([]); const [isModalOpen, setIsModalOpen] = useState(false); const [modalText, setModalText] = useState(); - - const [successAlertTitle, setSuccessAlertTitle] = useState(); - const [successAlertMessage, setSuccessAlertMessage] = useState(); - const [successAlertLink, setSuccessAlertLink] = useState(); - - const [failureAlertTitle, setFailureAlertTitle] = useState(); - const [failureAlertMessage, setFailureAlertMessage] = useState(); - + const [alertInfo, setAlertInfo] = useState(); const [validRepo, setValidRepo] = useState(); const [validCommit, setValidCommit] = useState(); const [validDocumentName, setValidDocumentName] = useState(); + interface AlertInfo { + type: 'success' | 'danger' | 'info'; + title: string; + message: string; + link?: string; + } + useEffect(() => { setValidRepo(ValidatedOptions.default); setValidCommit(ValidatedOptions.default); @@ -116,6 +116,13 @@ const DocumentInformation: React.FC = ({ const handleDocumentUpload = async () => { if (uploadedFiles.length > 0) { + const alertInfo: AlertInfo = { + type: 'info', + title: 'Document upload(s) in progress!', + message: 'Document upload(s) is in progress. You will be notified once the upload successfully completes.' + }; + setAlertInfo(alertInfo); + const fileContents: { fileName: string; fileContent: string }[] = []; await Promise.all( @@ -144,9 +151,13 @@ const DocumentInformation: React.FC = ({ }); if (!response.ok) { - setFailureAlertTitle('Failed to upload document'); - setFailureAlertMessage(`This upload failed. ${response.statusText}`); - new Error(response.statusText || 'Failed to upload document'); + const alertInfo: AlertInfo = { + type: 'danger', + title: 'Document upload failed', + message: `Upload failed for the added documents. ${response.statusText}` + }; + setAlertInfo(alertInfo); + new Error(response.statusText || 'Document upload failed'); return; } @@ -156,22 +167,21 @@ const DocumentInformation: React.FC = ({ setKnowledgeDocumentCommit(result.commitSha); setDocumentName(result.documentNames.join(', ')); // Populate the patterns field console.log('Files uploaded:', result.documentNames); - setSuccessAlertTitle('Document uploaded successfully!'); - setSuccessAlertMessage('Documents have been uploaded to your repo to be referenced in the knowledge submission.'); - setSuccessAlertLink(result.prUrl); + const alertInfo: AlertInfo = { + type: 'success', + title: 'Document uploaded successfully!', + message: 'Documents have been uploaded to your repo to be referenced in the knowledge submission.' + }; + if (result.prUrl !== '') { + alertInfo.link = result.prUrl; + } + setAlertInfo(alertInfo); } } }; const onCloseSuccessAlert = () => { - setSuccessAlertTitle(undefined); - setSuccessAlertMessage(undefined); - setSuccessAlertLink(undefined); - }; - - const onCloseFailureAlert = () => { - setFailureAlertTitle(undefined); - setFailureAlertMessage(undefined); + setAlertInfo(undefined); }; const handleAutomaticUpload = () => { @@ -209,17 +219,7 @@ const DocumentInformation: React.FC = ({ return (
- - Document Information * -

- ), - id: 'doc-info-id' - }} - titleDescription="Add the relevant document's information" - /> +
); diff --git a/src/components/Contribute/Knowledge/Submit/Submit.tsx b/src/components/Contribute/Knowledge/Github/Submit/Submit.tsx similarity index 95% rename from src/components/Contribute/Knowledge/Submit/Submit.tsx rename to src/components/Contribute/Knowledge/Github/Submit/Submit.tsx index d0c24bac..9d7416df 100644 --- a/src/components/Contribute/Knowledge/Submit/Submit.tsx +++ b/src/components/Contribute/Knowledge/Github/Submit/Submit.tsx @@ -1,10 +1,10 @@ import React from 'react'; import { Button } from '@patternfly/react-core/dist/dynamic/components/Button'; -import { ActionGroupAlertContent, KnowledgeFormData } from '..'; -import { AttributionData, KnowledgeYamlData } from '@/types'; +import { ActionGroupAlertContent } from '..'; +import { AttributionData, KnowledgeFormData, KnowledgeYamlData } from '@/types'; import { KnowledgeSchemaVersion } from '@/types/const'; import { dumpYaml } from '@/utils/yamlConfig'; -import { validateFields } from '../validation'; +import { validateFields } from '../../validation'; interface Props { disableAction: boolean; diff --git a/src/components/Contribute/Knowledge/Update/Update.tsx b/src/components/Contribute/Knowledge/Github/Update/Update.tsx similarity index 97% rename from src/components/Contribute/Knowledge/Update/Update.tsx rename to src/components/Contribute/Knowledge/Github/Update/Update.tsx index 4824c068..8961e2b7 100644 --- a/src/components/Contribute/Knowledge/Update/Update.tsx +++ b/src/components/Contribute/Knowledge/Github/Update/Update.tsx @@ -1,10 +1,10 @@ import React from 'react'; import { Button } from '@patternfly/react-core/dist/dynamic/components/Button'; -import { ActionGroupAlertContent, KnowledgeFormData } from '..'; -import { AttributionData, KnowledgeYamlData, PullRequestFile } from '@/types'; +import { ActionGroupAlertContent } from '..'; +import { AttributionData, KnowledgeFormData, KnowledgeYamlData, PullRequestFile } from '@/types'; import { KnowledgeSchemaVersion } from '@/types/const'; import { dumpYaml } from '@/utils/yamlConfig'; -import { validateFields } from '../validation'; +import { validateFields } from '../../validation'; import { amendCommit, getGitHubUsername, updatePullRequest } from '@/utils/github'; import { useSession } from 'next-auth/react'; import { useRouter } from 'next/navigation'; diff --git a/src/components/Experimental/ContributeLocal/Knowledge/index.tsx b/src/components/Contribute/Knowledge/Github/index.tsx similarity index 80% rename from src/components/Experimental/ContributeLocal/Knowledge/index.tsx rename to src/components/Contribute/Knowledge/Github/index.tsx index 7a7d03db..96f9b30d 100644 --- a/src/components/Experimental/ContributeLocal/Knowledge/index.tsx +++ b/src/components/Contribute/Knowledge/Github/index.tsx @@ -1,8 +1,8 @@ -// src/components/Experimental/ContributeLocal/Knowledge/index.tsx +// src/components/Contribute/Knowledge/Github/index.tsx 'use client'; import React, { useEffect, useMemo, useState } from 'react'; -import './knowledge.css'; -import { Alert, AlertActionCloseButton } from '@patternfly/react-core/dist/dynamic/components/Alert'; +import '../knowledge.css'; +import { Alert, AlertActionCloseButton, AlertGroup } from '@patternfly/react-core/dist/dynamic/components/Alert'; import { ActionGroup } from '@patternfly/react-core/dist/dynamic/components/Form'; import { getGitHubUsername } from '@/utils/github'; import { useSession } from 'next-auth/react'; @@ -10,9 +10,9 @@ import AuthorInformation from '@/components/Contribute/AuthorInformation'; import { FormType } from '@/components/Contribute/AuthorInformation'; import KnowledgeInformation from '@/components/Contribute/Knowledge/KnowledgeInformation/KnowledgeInformation'; import FilePathInformation from '@/components/Contribute/Knowledge/FilePathInformation/FilePathInformation'; -import DocumentInformation from '@/components/Contribute/Knowledge/DocumentInformation/DocumentInformation'; +import DocumentInformation from '@/components/Contribute/Knowledge/Github/DocumentInformation/DocumentInformation'; import AttributionInformation from '@/components/Contribute/Knowledge/AttributionInformation/AttributionInformation'; -import Submit from './SubmitLocal/Submit'; +import Submit from './Submit/Submit'; import { Breadcrumb } from '@patternfly/react-core/dist/dynamic/components/Breadcrumb'; import { BreadcrumbItem } from '@patternfly/react-core/dist/dynamic/components/Breadcrumb'; import { PageBreadcrumb } from '@patternfly/react-core/dist/dynamic/components/Page'; @@ -25,62 +25,18 @@ import { checkKnowledgeFormCompletion } from '@/components/Contribute/Knowledge/ import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants'; import { DownloadDropdown } from '@/components/Contribute/Knowledge/DownloadDropdown/DownloadDropdown'; import { ViewDropdown } from '@/components/Contribute/Knowledge/ViewDropdown/ViewDropdown'; -import Update from '@/components/Contribute/Knowledge/Update/Update'; -import { PullRequestFile } from '@/types'; +import Update from '@/components/Contribute/Knowledge/Github/Update/Update'; +import { KnowledgeEditFormData, KnowledgeFormData, KnowledgeYamlData, QuestionAndAnswerPair } from '@/types'; import { Button } from '@patternfly/react-core/dist/esm/components/Button/Button'; import { useRouter } from 'next/navigation'; import { autoFillKnowledgeFields } from '@/components/Contribute/Knowledge/AutoFill'; import { Spinner } from '@patternfly/react-core/dist/esm/components/Spinner'; import { Wizard, WizardStep } from '@patternfly/react-core/dist/esm/components/Wizard'; import { Content } from '@patternfly/react-core/dist/dynamic/components/Content'; -import ReviewSubmission from '@/components/Experimental/ReviewSubmission'; - -export interface QuestionAndAnswerPair { - immutable: boolean; - question: string; - isQuestionValid: ValidatedOptions; - questionValidationError?: string; - answer: string; - isAnswerValid: ValidatedOptions; - answerValidationError?: string; -} - -export interface SeedExample { - immutable: boolean; - isExpanded: boolean; - context: string; - isContextValid: ValidatedOptions; - validationError?: string; - questionAndAnswers: QuestionAndAnswerPair[]; -} - -export interface KnowledgeFormData { - email: string; - name: string; - submissionSummary: string; - domain: string; - documentOutline: string; - filePath: string; - seedExamples: SeedExample[]; - knowledgeDocumentRepositoryUrl: string; - knowledgeDocumentCommit: string; - documentName: string; - titleWork: string; - linkWork: string; - revision: string; - licenseWork: string; - creators: string; -} - -export interface KnowledgeEditFormData { - isEditForm: boolean; - knowledgeVersion: number; - pullRequestNumber: number; - branchName: string; - yamlFile: PullRequestFile; - attributionFile: PullRequestFile; - knowledgeFormData: KnowledgeFormData; -} +import ReviewSubmission from '@/components/Contribute/Knowledge/ReviewSubmission'; +import { Flex } from '@patternfly/react-core/dist/esm/layouts/Flex/Flex'; +import { FlexItem } from '@patternfly/react-core/dist/esm/layouts/Flex/FlexItem'; +import { YamlFileUploadModal } from '../../YamlFileUploadModal'; export interface ActionGroupAlertContent { title: string; @@ -95,8 +51,8 @@ export interface KnowledgeFormProps { knowledgeEditFormData?: KnowledgeEditFormData; } -export const KnowledgeFormLocal: React.FunctionComponent = ({ knowledgeEditFormData }) => { - const [deploymentType, setDeploymentType] = useState(); +export const KnowledgeFormGithub: React.FunctionComponent = ({ knowledgeEditFormData }) => { + const [devModeEnabled, setDevModeEnabled] = useState(); const { data: session } = useSession(); const [githubUsername, setGithubUsername] = useState(''); @@ -129,12 +85,13 @@ export const KnowledgeFormLocal: React.FunctionComponent = ( const [disableAction, setDisableAction] = useState(true); const [reset, setReset] = useState(false); + const [isModalOpen, setIsModalOpen] = React.useState(false); const router = useRouter(); const [activeStepIndex] = useState(1); - const emptySeedExample: SeedExample = { + const emptySeedExample: KnowledgeSeedExample = { immutable: true, isExpanded: false, context: '', @@ -164,7 +121,7 @@ export const KnowledgeFormLocal: React.FunctionComponent = ( ] }; - const [seedExamples, setSeedExamples] = useState([ + const [seedExamples, setSeedExamples] = useState([ emptySeedExample, emptySeedExample, emptySeedExample, @@ -176,7 +133,7 @@ export const KnowledgeFormLocal: React.FunctionComponent = ( const getEnvVariables = async () => { const res = await fetch('/api/envConfig'); const envConfig = await res.json(); - setDeploymentType(envConfig.DEPLOYMENT_TYPE); + setDevModeEnabled(envConfig.ENABLE_DEV_MODE === 'true'); }; getEnvVariables(); }, []); @@ -280,7 +237,7 @@ export const KnowledgeFormLocal: React.FunctionComponent = ( const handleContextInputChange = (seedExampleIndex: number, contextValue: string): void => { setSeedExamples( - seedExamples.map((seedExample: SeedExample, index: number) => + seedExamples.map((seedExample: KnowledgeSeedExample, index: number) => index === seedExampleIndex ? { ...seedExample, @@ -292,7 +249,7 @@ export const KnowledgeFormLocal: React.FunctionComponent = ( }; const handleContextBlur = (seedExampleIndex: number): void => { - const updatedSeedExamples = seedExamples.map((seedExample: SeedExample, index: number): SeedExample => { + const updatedSeedExamples = seedExamples.map((seedExample: KnowledgeSeedExample, index: number): KnowledgeSeedExample => { if (index === seedExampleIndex) { const { msg, status } = validateContext(seedExample.context); return { @@ -308,7 +265,7 @@ export const KnowledgeFormLocal: React.FunctionComponent = ( const handleQuestionInputChange = (seedExampleIndex: number, questionAndAnswerIndex: number, questionValue: string): void => { setSeedExamples( - seedExamples.map((seedExample: SeedExample, index: number) => + seedExamples.map((seedExample: KnowledgeSeedExample, index: number) => index === seedExampleIndex ? { ...seedExample, @@ -328,7 +285,7 @@ export const KnowledgeFormLocal: React.FunctionComponent = ( const handleQuestionBlur = (seedExampleIndex: number, questionAndAnswerIndex: number): void => { setSeedExamples( - seedExamples.map((seedExample: SeedExample, index: number) => + seedExamples.map((seedExample: KnowledgeSeedExample, index: number) => index === seedExampleIndex ? { ...seedExample, @@ -351,7 +308,7 @@ export const KnowledgeFormLocal: React.FunctionComponent = ( const handleAnswerInputChange = (seedExampleIndex: number, questionAndAnswerIndex: number, answerValue: string): void => { setSeedExamples( - seedExamples.map((seedExample: SeedExample, index: number) => + seedExamples.map((seedExample: KnowledgeSeedExample, index: number) => index === seedExampleIndex ? { ...seedExample, @@ -371,7 +328,7 @@ export const KnowledgeFormLocal: React.FunctionComponent = ( const handleAnswerBlur = (seedExampleIndex: number, questionAndAnswerIndex: number): void => { setSeedExamples( - seedExamples.map((seedExample: SeedExample, index: number) => + seedExamples.map((seedExample: KnowledgeSeedExample, index: number) => index === seedExampleIndex ? { ...seedExample, @@ -443,6 +400,32 @@ export const KnowledgeFormLocal: React.FunctionComponent = ( setSeedExamples(autoFillKnowledgeFields.seedExamples); }; + const yamlSeedExampleToFormSeedExample = ( + yamlSeedExamples: { context: string; questions_and_answers: { question: string; answer: string }[] }[] + ) => { + return yamlSeedExamples.map((yamlSeedExample) => ({ + immutable: true, + isExpanded: false, + context: yamlSeedExample.context ?? '', + isContextValid: ValidatedOptions.default, + questionAndAnswers: yamlSeedExample.questions_and_answers.map((questionAndAnswer) => ({ + question: questionAndAnswer.question ?? '', + answer: questionAndAnswer.answer ?? '' + })) + })) as KnowledgeSeedExample[]; + }; + + const onYamlUploadKnowledgeFillForm = (data: KnowledgeYamlData): void => { + setName(data.created_by ?? ''); + setDocumentOutline(data.document_outline ?? ''); + setSubmissionSummary(data.document_outline ?? ''); + setDomain(data.domain ?? ''); + setKnowledgeDocumentRepositoryUrl(data.document.repo ?? ''); + setKnowledgeDocumentCommit(data.document.commit ?? ''); + setDocumentName(data.document.patterns.join(', ') ?? ''); + setSeedExamples(yamlSeedExampleToFormSeedExample(data.seed_examples)); + }; + const knowledgeFormData: KnowledgeFormData = { email: email, name: name, @@ -591,17 +574,33 @@ export const KnowledgeFormLocal: React.FunctionComponent = ( - - Knowledge Contribution - + + + + Knowledge Contribution + + + + {devModeEnabled && ( + + )} + {' '} + + + - {deploymentType === 'dev' && ( - - )} + {steps.map((step) => ( @@ -612,27 +611,29 @@ export const KnowledgeFormLocal: React.FunctionComponent = ( {actionGroupAlertContent && ( - } - > -

- {actionGroupAlertContent.waitAlert && } - {actionGroupAlertContent.message} -
- {!actionGroupAlertContent.waitAlert && - actionGroupAlertContent.success && - actionGroupAlertContent.url && - actionGroupAlertContent.url.trim().length > 0 && ( - - View your new branch - - )} -

-
+ + } + > +

+ {actionGroupAlertContent.waitAlert && } + {actionGroupAlertContent.message} +
+ {!actionGroupAlertContent.waitAlert && + actionGroupAlertContent.success && + actionGroupAlertContent.url && + actionGroupAlertContent.url.trim().length > 0 && ( + + View your new branch + + )} +

+
+
)} @@ -652,7 +653,7 @@ export const KnowledgeFormLocal: React.FunctionComponent = ( disableAction={disableAction} knowledgeFormData={knowledgeFormData} setActionGroupAlertContent={setActionGroupAlertContent} - email={email} + githubUsername={githubUsername} resetForm={resetForm} /> )} @@ -667,4 +668,4 @@ export const KnowledgeFormLocal: React.FunctionComponent = ( ); }; -export default KnowledgeFormLocal; +export default KnowledgeFormGithub; diff --git a/src/components/Contribute/Knowledge/KnowledgeInformation/KnowledgeInformation.tsx b/src/components/Contribute/Knowledge/KnowledgeInformation/KnowledgeInformation.tsx index 677d72df..4dc0435c 100644 --- a/src/components/Contribute/Knowledge/KnowledgeInformation/KnowledgeInformation.tsx +++ b/src/components/Contribute/Knowledge/KnowledgeInformation/KnowledgeInformation.tsx @@ -6,8 +6,8 @@ import { HelperText } from '@patternfly/react-core/dist/dynamic/components/Helpe import { HelperTextItem } from '@patternfly/react-core/dist/dynamic/components/HelperText'; import ExclamationCircleIcon from '@patternfly/react-icons/dist/dynamic/icons/exclamation-circle-icon'; import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants'; -import { KnowledgeFormData } from '..'; import { checkKnowledgeFormCompletion } from '../validation'; +import { KnowledgeFormData } from '@/types'; interface Props { reset: boolean; diff --git a/src/components/Contribute/Knowledge/KnowledgeQuestionAnswerPairs/KnowledgeQuestionAnswerPairs.tsx b/src/components/Contribute/Knowledge/KnowledgeQuestionAnswerPairs/KnowledgeQuestionAnswerPairs.tsx index e57cc599..e464529d 100644 --- a/src/components/Contribute/Knowledge/KnowledgeQuestionAnswerPairs/KnowledgeQuestionAnswerPairs.tsx +++ b/src/components/Contribute/Knowledge/KnowledgeQuestionAnswerPairs/KnowledgeQuestionAnswerPairs.tsx @@ -2,13 +2,13 @@ import React from 'react'; import { FormFieldGroupHeader, FormGroup, FormHelperText } from '@patternfly/react-core/dist/dynamic/components/Form'; import { TextArea } from '@patternfly/react-core/dist/dynamic/components/TextArea'; import { ExclamationCircleIcon } from '@patternfly/react-icons/dist/dynamic/icons/'; -import { QuestionAndAnswerPair, SeedExample } from '..'; import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants'; import { HelperText } from '@patternfly/react-core/dist/dynamic/components/HelperText'; import { HelperTextItem } from '@patternfly/react-core/dist/dynamic/components/HelperText'; +import { KnowledgeSeedExample, QuestionAndAnswerPair } from '@/types'; interface Props { - seedExample: SeedExample; + seedExample: KnowledgeSeedExample; seedExampleIndex: number; handleContextInputChange: (seedExampleIndex: number, contextValue: string) => void; handleContextBlur: (seedExampleIndex: number) => void; diff --git a/src/components/Contribute/Knowledge/KnowledgeSeedExample/KnowledgeSeedExample.tsx b/src/components/Contribute/Knowledge/KnowledgeSeedExample/KnowledgeSeedExample.tsx index 9649763f..5e984b2b 100644 --- a/src/components/Contribute/Knowledge/KnowledgeSeedExample/KnowledgeSeedExample.tsx +++ b/src/components/Contribute/Knowledge/KnowledgeSeedExample/KnowledgeSeedExample.tsx @@ -3,11 +3,11 @@ import React from 'react'; import { Accordion, AccordionItem, AccordionContent, AccordionToggle } from '@patternfly/react-core/dist/dynamic/components/Accordion'; import { FormFieldGroupHeader } from '@patternfly/react-core/dist/dynamic/components/Form'; import KnowledgeQuestionAnswerPairs from '../KnowledgeQuestionAnswerPairs/KnowledgeQuestionAnswerPairs'; -import { SeedExample } from '..'; +import type { KnowledgeSeedExample } from '@/types'; import ExternalLinkAltIcon from '@patternfly/react-icons/dist/esm/icons/external-link-alt-icon'; interface Props { - seedExamples: SeedExample[]; + seedExamples: KnowledgeSeedExample[]; handleContextInputChange: (seedExampleIndex: number, contextValue: string) => void; handleContextBlur: (seedExampleIndex: number) => void; handleQuestionInputChange: (seedExampleIndex: number, questionAndAnswerIndex: number, questionValue: string) => void; @@ -51,7 +51,7 @@ const KnowledgeSeedExample: React.FC = ({ /> - {seedExamples.map((seedExample: SeedExample, seedExampleIndex: number) => ( + {seedExamples.map((seedExample: KnowledgeSeedExample, seedExampleIndex: number) => ( toggleSeedExampleExpansion(seedExampleIndex)} id={`seed-example-toggle-${seedExampleIndex}`}> Seed Example {seedExampleIndex + 1} {seedExample.immutable && *} diff --git a/src/components/Contribute/Knowledge/Native/DocumentInformation/DocumentInformation.tsx b/src/components/Contribute/Knowledge/Native/DocumentInformation/DocumentInformation.tsx new file mode 100644 index 00000000..3b81185c --- /dev/null +++ b/src/components/Contribute/Knowledge/Native/DocumentInformation/DocumentInformation.tsx @@ -0,0 +1,408 @@ +// src/components/Contribute/Knowledge/Native/DocumentInformation/DocumentInformation.tsx +import React, { useEffect, useState } from 'react'; +import { FormFieldGroupHeader, FormGroup, FormHelperText } from '@patternfly/react-core/dist/dynamic/components/Form'; +import { Button } from '@patternfly/react-core/dist/dynamic/components/Button'; +import { TextInput } from '@patternfly/react-core/dist/dynamic/components/TextInput'; +import { Alert, AlertActionLink, AlertActionCloseButton } from '@patternfly/react-core/dist/dynamic/components/Alert'; +import { HelperText } from '@patternfly/react-core/dist/dynamic/components/HelperText'; +import { HelperTextItem } from '@patternfly/react-core/dist/dynamic/components/HelperText'; +import ExclamationCircleIcon from '@patternfly/react-icons/dist/dynamic/icons/exclamation-circle-icon'; +import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants'; +import { Modal, ModalVariant } from '@patternfly/react-core/dist/esm/deprecated/components/Modal/Modal'; +import { UploadFile } from '@/components/Contribute/Knowledge/UploadFile'; +import { checkKnowledgeFormCompletion } from '@/components/Contribute/Knowledge/validation'; +import { KnowledgeFormData } from '@/types'; + +interface Props { + reset: boolean; + isEditForm?: boolean; + knowledgeFormData: KnowledgeFormData; + setDisableAction: React.Dispatch>; + + knowledgeDocumentRepositoryUrl: string; + setKnowledgeDocumentRepositoryUrl: React.Dispatch>; + + knowledgeDocumentCommit: string; + setKnowledgeDocumentCommit: React.Dispatch>; + + documentName: string; + setDocumentName: React.Dispatch>; +} + +interface AlertInfo { + type: 'success' | 'danger' | 'info'; + title: string; + message: string; + link?: string; +} + +const DocumentInformation: React.FC = ({ + reset, + isEditForm, + knowledgeFormData, + setDisableAction, + knowledgeDocumentRepositoryUrl, + setKnowledgeDocumentRepositoryUrl, + knowledgeDocumentCommit, + setKnowledgeDocumentCommit, + documentName, + setDocumentName +}) => { + const [useFileUpload, setUseFileUpload] = useState(true); + const [uploadedFiles, setUploadedFiles] = useState([]); + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalText, setModalText] = useState(); + + const [successAlertTitle, setSuccessAlertTitle] = useState(); + const [successAlertMessage, setSuccessAlertMessage] = useState(); + const [successAlertLink, setSuccessAlertLink] = useState(); + + const [failureAlertTitle, setFailureAlertTitle] = useState(); + const [failureAlertMessage, setFailureAlertMessage] = useState(); + const [alertInfo, setAlertInfo] = useState(); + + const [validRepo, setValidRepo] = useState(ValidatedOptions.default); + const [validCommit, setValidCommit] = useState(ValidatedOptions.default); + const [validDocumentName, setValidDocumentName] = useState(ValidatedOptions.default); + + useEffect(() => { + setValidRepo(ValidatedOptions.default); + setValidCommit(ValidatedOptions.default); + setValidDocumentName(ValidatedOptions.default); + }, [reset]); + + useEffect(() => { + if (isEditForm) { + setValidRepo(ValidatedOptions.success); + setValidCommit(ValidatedOptions.success); + setValidDocumentName(ValidatedOptions.success); + } + }, [isEditForm]); + + const validateRepo = (repoStr: string) => { + const repo = repoStr.trim(); + if (repo.length === 0) { + setDisableAction(true); + setValidRepo(ValidatedOptions.error); + return; + } + try { + new URL(repo); + setValidRepo(ValidatedOptions.success); + setDisableAction(!checkKnowledgeFormCompletion(knowledgeFormData)); + return; + } catch (e) { + setDisableAction(true); + setValidRepo(ValidatedOptions.warning); + return; + } + }; + + const validateCommit = (commitStr: string) => { + const commit = commitStr.trim(); + if (commit.length > 0) { + setValidCommit(ValidatedOptions.success); + setDisableAction(!checkKnowledgeFormCompletion(knowledgeFormData)); + return; + } + setDisableAction(true); + setValidCommit(ValidatedOptions.error); + return; + }; + + const validateDocumentName = (document: string) => { + const documentNameStr = document.trim(); + if (documentNameStr.length > 0) { + setValidDocumentName(ValidatedOptions.success); + setDisableAction(!checkKnowledgeFormCompletion(knowledgeFormData)); + return; + } + setDisableAction(true); + setValidDocumentName(ValidatedOptions.error); + return; + }; + + const handleFilesChange = (files: File[]) => { + setUploadedFiles(files); + }; + + const handleDocumentUpload = async () => { + if (uploadedFiles.length > 0) { + const alertInfo: AlertInfo = { + type: 'info', + title: 'Document upload(s) in progress!', + message: 'Document upload(s) is in progress. You will be notified once the upload successfully completes.' + }; + setAlertInfo(alertInfo); + + const fileContents: { fileName: string; fileContent: string }[] = []; + + await Promise.all( + uploadedFiles.map( + (file) => + new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (e) => { + const fileContent = e.target!.result as string; + fileContents.push({ fileName: file.name, fileContent }); + resolve(); + }; + reader.onerror = reject; + reader.readAsText(file); + }) + ) + ); + + if (fileContents.length === uploadedFiles.length) { + try { + const response = await fetch('/api/native/upload', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ files: fileContents }) + }); + + if (response.status === 201) { + const result = await response.json(); + console.log('Files uploaded result:', result); + + setSuccessAlertTitle('Document uploaded successfully!'); + setSuccessAlertMessage('Documents have been uploaded to your repo to be referenced in the knowledge submission.'); + if (result.prUrl && result.prUrl.trim() !== '') { + setSuccessAlertLink(result.prUrl); + } else { + setSuccessAlertLink(undefined); + } + } else { + console.error('Upload failed:', response.statusText); + setFailureAlertTitle('Failed to upload document'); + setFailureAlertMessage(`This upload failed. ${response.statusText}`); + } + } catch (error) { + console.error('Upload error:', error); + setFailureAlertTitle('Failed to upload document'); + setFailureAlertMessage(`This upload failed. ${(error as Error).message}`); + } + } + } + }; + + const onCloseSuccessAlert = () => { + setSuccessAlertTitle(undefined); + setSuccessAlertMessage(undefined); + setSuccessAlertLink(undefined); + }; + + const onCloseFailureAlert = () => { + setFailureAlertTitle(undefined); + setFailureAlertMessage(undefined); + }; + + const handleAutomaticUpload = () => { + if (knowledgeDocumentRepositoryUrl.length > 0 || knowledgeDocumentCommit.length > 0 || documentName.length > 0) { + setModalText('Switching to automatic upload will clear the document information. Are you sure you want to continue?'); + setIsModalOpen(true); + } else { + setUseFileUpload(true); + } + }; + + const handleManualUpload = () => { + if (uploadedFiles.length > 0) { + setModalText('Switching to manual upload will clear the uploaded files. Are you sure you want to continue?'); + setIsModalOpen(true); + } else { + setUseFileUpload(false); + } + }; + + const handleModalContinue = () => { + if (useFileUpload) { + setUploadedFiles([]); + } else { + console.log('Switching to manual entry - clearing repository and document info'); + setKnowledgeDocumentRepositoryUrl(''); + setValidRepo(ValidatedOptions.default); + setKnowledgeDocumentCommit(''); + setValidCommit(ValidatedOptions.default); + setDocumentName(''); + setValidDocumentName(ValidatedOptions.default); + } + setUseFileUpload(!useFileUpload); + setIsModalOpen(false); + }; + + return ( +
+ + Document Information * +

+ ), + id: 'doc-info-id' + }} + titleDescription="Add the relevant document's information" + /> + +
+ + +
+
+ setIsModalOpen(false)} + actions={[ + , + + ]} + > +

{modalText}

+
+ {!useFileUpload ? ( + <> + + setKnowledgeDocumentRepositoryUrl(value)} + onBlur={() => validateRepo(knowledgeDocumentRepositoryUrl)} + /> + {validRepo === ValidatedOptions.error && ( + + + } variant={validRepo}> + Required field + + + + )} + {validRepo === ValidatedOptions.warning && ( + + + } variant="error"> + Please enter a valid URL. + + + + )} + + + setKnowledgeDocumentCommit(value)} + onBlur={() => validateCommit(knowledgeDocumentCommit)} + /> + {validCommit === ValidatedOptions.error && ( + + + } variant={validCommit}> + Valid commit SHA is required. + + + + )} + + + setDocumentName(value)} + onBlur={() => validateDocumentName(documentName)} + /> + {validDocumentName === ValidatedOptions.error && ( + + + } variant={validDocumentName}> + Required field + + + + )} + + + ) : ( + <> + + + + )} + + {/* Informational Alert */} + {alertInfo && ( + setAlertInfo(undefined)} />}> + {alertInfo.message} + {alertInfo.link && ( + + View it here + + )} + + )} + + {/* Success Alert */} + {successAlertTitle && successAlertMessage && ( + } + actionLinks={ + successAlertLink ? ( + + View it here + + ) : null + } + > + {successAlertMessage} + + )} + + {/* Failure Alert */} + {failureAlertTitle && failureAlertMessage && ( + }> + {failureAlertMessage} + + )} +
+ ); +}; + +export default DocumentInformation; diff --git a/src/components/Contribute/Knowledge/Native/KnowledgeQuestionAnswerPairsNative/KnowledgeQuestionAnswerPairs.tsx b/src/components/Contribute/Knowledge/Native/KnowledgeQuestionAnswerPairsNative/KnowledgeQuestionAnswerPairs.tsx new file mode 100644 index 00000000..a0f78b8f --- /dev/null +++ b/src/components/Contribute/Knowledge/Native/KnowledgeQuestionAnswerPairsNative/KnowledgeQuestionAnswerPairs.tsx @@ -0,0 +1,448 @@ +// src/components/Contribute/Knowledge/KnowledgeQuestionAnswerPairs/KnowledgeQuestionAnswerPairs.tsx +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { FormFieldGroupHeader, FormGroup, FormHelperText } from '@patternfly/react-core/dist/dynamic/components/Form'; +import { TextArea } from '@patternfly/react-core/dist/dynamic/components/TextArea'; +import { ExclamationCircleIcon } from '@patternfly/react-icons/dist/dynamic/icons/'; +import { ValidatedOptions } from '@patternfly/react-core/dist/esm/helpers/constants'; +import { HelperText } from '@patternfly/react-core/dist/dynamic/components/HelperText'; +import { HelperTextItem } from '@patternfly/react-core/dist/dynamic/components/HelperText'; +import { KnowledgeSeedExample, QuestionAndAnswerPair } from '@/types'; +import { Modal, ModalVariant } from '@patternfly/react-core/dist/dynamic/components/Modal'; +import { Tooltip } from '@patternfly/react-core/dist/esm/components/Tooltip/Tooltip'; +import { CatalogIcon } from '@patternfly/react-icons/dist/esm/icons/catalog-icon'; +import { Button } from '@patternfly/react-core/dist/dynamic/components/Button'; +import { Spinner } from '@patternfly/react-core/dist/dynamic/components/Spinner'; +import { ExpandableSection } from '@patternfly/react-core/dist/esm/components/ExpandableSection/ExpandableSection'; +import { Content } from '@patternfly/react-core/dist/dynamic/components/Content'; +import { Switch } from '@patternfly/react-core/dist/dynamic/components/Switch'; +import { Card, CardBody, CardHeader } from '@patternfly/react-core/dist/dynamic/components/Card'; +import { Stack, StackItem } from '@patternfly/react-core/dist/dynamic/layouts/Stack'; +import { Alert } from '@patternfly/react-core/dist/dynamic/components/Alert'; + +interface KnowledgeFile { + filename: string; + content: string; + commitSha: string; + commitDate?: string; +} + +interface Props { + seedExample: KnowledgeSeedExample; + seedExampleIndex: number; + handleContextInputChange: (seedExampleIndex: number, contextValue: string) => void; + handleContextBlur: (seedExampleIndex: number) => void; + handleQuestionInputChange: (seedExampleIndex: number, questionAndAnswerIndex: number, questionValue: string) => void; + handleQuestionBlur: (seedExampleIndex: number, questionAndAnswerIndex: number) => void; + handleAnswerInputChange: (seedExampleIndex: number, questionAndAnswerIndex: number, answerValue: string) => void; + handleAnswerBlur: (seedExampleIndex: number, questionAndAnswerIndex: number) => void; + addDocumentInfo: (repositoryUrl: string, commitSha: string, docName: string) => void; + repositoryUrl: string; + commitSha: string; +} + +const KnowledgeQuestionAnswerPairsNative: React.FC = ({ + seedExample, + seedExampleIndex, + handleContextInputChange, + handleContextBlur, + handleQuestionInputChange, + handleQuestionBlur, + handleAnswerInputChange, + handleAnswerBlur, + addDocumentInfo, + repositoryUrl, + commitSha +}) => { + const [isModalOpen, setIsModalOpen] = useState(false); + const [knowledgeFiles, setKnowledgeFiles] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(''); + const [expandedFiles, setExpandedFiles] = useState>({}); + const [selectedWordCount, setSelectedWordCount] = useState(0); + const [showAllCommits, setShowAllCommits] = useState(false); + + // Ref for the
 elements to track selections TODO: figure out how to make text expansions taller in PF without a custom-pre
+  const preRefs = useRef>({});
+
+  const LOCAL_TAXONOMY_DOCS_ROOT_DIR =
+    process.env.NEXT_PUBLIC_LOCAL_TAXONOMY_DOCS_ROOT_DIR || '/home/yourusername/.instructlab-ui/taxonomy-knowledge-docs';
+
+  const fetchKnowledgeFiles = async () => {
+    setIsLoading(true);
+    setError('');
+    try {
+      const response = await fetch('/api/native/git/knowledge-files', {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({ branchName: 'main', action: 'diff' })
+      });
+
+      const result = await response.json();
+      if (response.ok) {
+        setKnowledgeFiles(result.files);
+        console.log('Fetched knowledge files:', result.files);
+      } else {
+        setError(result.error || 'Failed to fetch knowledge files.');
+        console.error('Error fetching knowledge files:', result.error);
+      }
+    } catch (err) {
+      setError('An error occurred while fetching knowledge files.');
+      console.error('Error fetching knowledge files:', err);
+    } finally {
+      setIsLoading(false);
+    }
+  };
+
+  const handleOpenModal = () => {
+    setIsModalOpen(true);
+    fetchKnowledgeFiles();
+  };
+
+  const handleCloseModal = () => {
+    setIsModalOpen(false);
+    setKnowledgeFiles([]);
+    setError('');
+    setSelectedWordCount(0);
+    setShowAllCommits(false);
+    window.getSelection()?.removeAllRanges();
+  };
+
+  const handleUseSelectedText = (file: KnowledgeFile) => {
+    const selection = window.getSelection();
+    const selectedText = selection?.toString().trim();
+
+    if (!selectedText) {
+      alert('Please select the text you want to use as context.');
+      return;
+    }
+
+    repositoryUrl = `${LOCAL_TAXONOMY_DOCS_ROOT_DIR}/${file.filename}`;
+    const commitShaValue = file.commitSha;
+    const docName = file.filename;
+
+    console.log(
+      `handleUseSelectedText: selectedText="${selectedText}", repositoryUrl=${repositoryUrl}, commitSha=${commitShaValue}, docName=${docName}`
+    );
+
+    handleContextInputChange(seedExampleIndex, selectedText);
+    handleContextBlur(seedExampleIndex);
+    addDocumentInfo(repositoryUrl, commitShaValue, docName);
+    handleCloseModal();
+  };
+
+  const updateSelectedWordCount = (filename: string) => {
+    const selection = window.getSelection();
+    const preElement = preRefs.current[filename];
+    if (selection && preElement) {
+      const anchorNode = selection.anchorNode;
+      const focusNode = selection.focusNode;
+
+      if (preElement.contains(anchorNode) && preElement.contains(focusNode)) {
+        const selectedText = selection.toString().trim();
+        const wordCount = selectedText.split(/\s+/).filter((word) => word.length > 0).length;
+        setSelectedWordCount(wordCount);
+      } else {
+        setSelectedWordCount(0);
+      }
+    }
+  };
+
+  // Attach event listeners for selection changes
+  useEffect(() => {
+    if (isModalOpen) {
+      const handleSelectionChange = () => {
+        // Iterate through all expanded files and update word count
+        Object.keys(expandedFiles).forEach((filename) => {
+          if (expandedFiles[filename]) {
+            updateSelectedWordCount(filename);
+          }
+        });
+      };
+      document.addEventListener('selectionchange', handleSelectionChange);
+      return () => {
+        document.removeEventListener('selectionchange', handleSelectionChange);
+      };
+    } else {
+      setSelectedWordCount(0);
+    }
+  }, [isModalOpen, expandedFiles]);
+
+  const toggleFileContent = (filename: string) => {
+    setExpandedFiles((prev) => ({
+      ...prev,
+      [filename]: !prev[filename]
+    }));
+    console.log(`toggleFileContent: filename=${filename}, expanded=${!expandedFiles[filename]}`);
+  };
+
+  // Group files by commitSha
+  const groupedFiles = knowledgeFiles.reduce>((acc, file) => {
+    if (!acc[file.commitSha]) {
+      acc[file.commitSha] = [];
+    }
+    acc[file.commitSha].push(file);
+    return acc;
+  }, {});
+
+  // Extract commit dates for sorting
+  const commitDateMap: Record = {};
+  knowledgeFiles.forEach((file) => {
+    if (file.commitDate && !commitDateMap[file.commitSha]) {
+      commitDateMap[file.commitSha] = file.commitDate;
+    }
+  });
+
+  // Sort the commit SHAs based on commitDate in descending order (latest first)
+  const sortedCommitShas = Object.keys(groupedFiles).sort((a, b) => {
+    const dateA = new Date(commitDateMap[a] || '').getTime();
+    const dateB = new Date(commitDateMap[b] || '').getTime();
+    return dateB - dateA;
+  });
+
+  // Enforce single commit SHA and repository URL
+  const isSameCommit = (fileCommitSha: string): boolean => {
+    if (!commitSha) {
+      return true;
+    }
+    return fileCommitSha === commitSha;
+  };
+
+  // Determine which commits to display based on the toggle
+  const commitsToDisplay = showAllCommits ? sortedCommitShas : sortedCommitShas.slice(0, 1);
+
+  const setPreRef = useCallback(
+    (filename: string) => (el: HTMLPreElement | null) => {
+      preRefs.current[filename] = el;
+    },
+    []
+  );
+
+  return (
+    
+      Select context from your knowledge files
} position="top"> + + + +