mirror of
https://github.com/cfngc4594/monaco-editor-lsp-next.git
synced 2025-07-04 17:30:52 +00:00
feat: replace dockview with flexlayout-react
This commit is contained in:
parent
03f150214d
commit
941f1a74fa
@ -2,7 +2,7 @@
|
||||
|
||||
# 升级到 Node.js v20 或更高版本,以解决 `ReferenceError: File is not defined` 问题
|
||||
# 参考链接:https://github.com/vercel/next.js/discussions/56032
|
||||
FROM dockerp.com/node:22-alpine AS base
|
||||
FROM docker.1ms.run/node:22-alpine AS base
|
||||
|
||||
# 仅在需要时安装依赖
|
||||
FROM base AS deps
|
||||
|
91
bun.lock
91
bun.lock
@ -4,6 +4,7 @@
|
||||
"": {
|
||||
"name": "monaco-editor-lsp-next",
|
||||
"dependencies": {
|
||||
"@ai-sdk/deepseek": "^0.2.14",
|
||||
"@ai-sdk/openai": "^1.3.0",
|
||||
"@ai-sdk/react": "^1.2.0",
|
||||
"@auth/prisma-adapter": "^2.8.0",
|
||||
@ -32,13 +33,14 @@
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"@types/vscode": "^1.97.0",
|
||||
"ai": "^4.2.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"devicons-react": "^1.4.0",
|
||||
"dockerode": "^4.0.4",
|
||||
"dockview": "^4.2.1",
|
||||
"flexlayout-react": "^0.7.15",
|
||||
"framer-motion": "^12.7.3",
|
||||
"github-markdown-css": "^5.8.1",
|
||||
"lucide-react": "^0.482.0",
|
||||
@ -73,7 +75,6 @@
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@shikijs/monaco": "^3.2.1",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/dockerode": "^3.3.35",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
@ -96,8 +97,12 @@
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@ai-sdk/deepseek": ["@ai-sdk/deepseek@0.2.14", "https://registry.npmmirror.com/@ai-sdk/deepseek/-/deepseek-0.2.14.tgz", { "dependencies": { "@ai-sdk/openai-compatible": "0.2.14", "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-TISD1FzBWuQkHEHoVustoJILV33ZNgfYxeTkq1xU2vHEZuWTGZV7/IlXixyFsfqDCdVgrbLeIABk5FuCw7niLg=="],
|
||||
|
||||
"@ai-sdk/openai": ["@ai-sdk/openai@1.3.22", "https://registry.npmmirror.com/@ai-sdk/openai/-/openai-1.3.22.tgz", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-QwA+2EkG0QyjVR+7h6FE7iOu2ivNqAVMm9UJZkVxxTk5OIq5fFJDTEI/zICEMuHImTTXR2JjsL6EirJ28Jc4cw=="],
|
||||
|
||||
"@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@0.2.14", "https://registry.npmmirror.com/@ai-sdk/openai-compatible/-/openai-compatible-0.2.14.tgz", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8" }, "peerDependencies": { "zod": "^3.0.0" } }, "sha512-icjObfMCHKSIbywijaoLdZ1nSnuRnWgMEMLgwoxPJgxsUHMx0aVORnsLUid4SPtdhHI3X2masrt6iaEQLvOSFw=="],
|
||||
|
||||
"@ai-sdk/provider": ["@ai-sdk/provider@1.1.3", "https://registry.npmmirror.com/@ai-sdk/provider/-/provider-1.1.3.tgz", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg=="],
|
||||
|
||||
"@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@2.2.8", "https://registry.npmmirror.com/@ai-sdk/provider-utils/-/provider-utils-2.2.8.tgz", { "dependencies": { "@ai-sdk/provider": "1.1.3", "nanoid": "^3.3.8", "secure-json-parse": "^2.7.0" }, "peerDependencies": { "zod": "^3.23.8" } }, "sha512-fqhG+4sCVv8x7nFzYnFo19ryhAa3w096Kmc3hWxMQfW/TubPOmt3A6tYZhl4mUfQWWQMsuSkLrtjlWuXBVSGQA=="],
|
||||
@ -278,8 +283,6 @@
|
||||
|
||||
"@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "https://registry.npmmirror.com/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="],
|
||||
|
||||
"@mapbox/node-pre-gyp": ["@mapbox/node-pre-gyp@1.0.11", "https://registry.npmmirror.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", { "dependencies": { "detect-libc": "^2.0.0", "https-proxy-agent": "^5.0.0", "make-dir": "^3.1.0", "node-fetch": "^2.6.7", "nopt": "^5.0.0", "npmlog": "^5.0.1", "rimraf": "^3.0.2", "semver": "^7.3.5", "tar": "^6.1.11" }, "bin": { "node-pre-gyp": "bin/node-pre-gyp" } }, "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ=="],
|
||||
|
||||
"@mdx-js/mdx": ["@mdx-js/mdx@3.1.0", "https://registry.npmmirror.com/@mdx-js/mdx/-/mdx-3.1.0.tgz", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw=="],
|
||||
|
||||
"@mdx-js/react": ["@mdx-js/react@3.1.0", "https://registry.npmmirror.com/@mdx-js/react/-/react-3.1.0.tgz", { "dependencies": { "@types/mdx": "^2.0.0" }, "peerDependencies": { "@types/react": ">=16", "react": ">=16" } }, "sha512-QjHtSaoameoalGnKDT3FoIl4+9RwyTmo9ZJGBdLOks/YOiWHoRDI3PUwEzOE7kEmGcV3AFcp9K6dYu9rEuKLAQ=="],
|
||||
@ -486,8 +489,6 @@
|
||||
|
||||
"@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.9.0.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="],
|
||||
|
||||
"@types/bcrypt": ["@types/bcrypt@5.0.2", "https://registry.npmmirror.com/@types/bcrypt/-/bcrypt-5.0.2.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-6atioO8Y75fNcbmj0G7UjI9lXN2pQ/IGJ2FWT4a/btd0Lk9lQalHLKhkgKVZ3r+spnmWUKfbMi1GEe9wyHQfNQ=="],
|
||||
|
||||
"@types/debug": ["@types/debug@4.1.12", "https://registry.npmmirror.com/@types/debug/-/debug-4.1.12.tgz", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
|
||||
|
||||
"@types/diff-match-patch": ["@types/diff-match-patch@1.0.36", "https://registry.npmmirror.com/@types/diff-match-patch/-/diff-match-patch-1.0.36.tgz", {}, "sha512-xFdR6tkm0MWvBfO8xXCSsinYxHcqkQUlcHeSpMC2ukzOb6lwQAfDmW+Qt0AvlGd8HpsS28qKsB+oPeJn9I39jg=="],
|
||||
@ -580,16 +581,12 @@
|
||||
|
||||
"@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.7.2", "https://registry.npmmirror.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.7.2.tgz", { "os": "win32", "cpu": "x64" }, "sha512-friS8NEQfHaDbkThxopGk+LuE5v3iY0StruifjQEt7SLbA46OnfgMO15sOTkbpJkol6RB+1l1TYPXh0sCddpvA=="],
|
||||
|
||||
"abbrev": ["abbrev@1.1.1", "https://registry.npmmirror.com/abbrev/-/abbrev-1.1.1.tgz", {}, "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="],
|
||||
|
||||
"accepts": ["accepts@2.0.0", "https://registry.npmmirror.com/accepts/-/accepts-2.0.0.tgz", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
|
||||
|
||||
"acorn": ["acorn@8.14.1", "https://registry.npmmirror.com/acorn/-/acorn-8.14.1.tgz", { "bin": { "acorn": "bin/acorn" } }, "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg=="],
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||
|
||||
"agent-base": ["agent-base@6.0.2", "https://registry.npmmirror.com/agent-base/-/agent-base-6.0.2.tgz", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
|
||||
|
||||
"ai": ["ai@4.3.15", "https://registry.npmmirror.com/ai/-/ai-4.3.15.tgz", { "dependencies": { "@ai-sdk/provider": "1.1.3", "@ai-sdk/provider-utils": "2.2.8", "@ai-sdk/react": "1.2.12", "@ai-sdk/ui-utils": "1.2.11", "@opentelemetry/api": "1.9.0", "jsondiffpatch": "0.6.0" }, "peerDependencies": { "react": "^18 || ^19 || ^19.0.0-rc", "zod": "^3.23.8" }, "optionalPeers": ["react"] }, "sha512-TYKRzbWg6mx/pmTadlAEIhuQtzfHUV0BbLY72+zkovXwq/9xhcH24IlQmkyBpElK6/4ArS0dHdOOtR1jOPVwtg=="],
|
||||
|
||||
"ajv": ["ajv@6.12.6", "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
|
||||
@ -602,10 +599,6 @@
|
||||
|
||||
"anymatch": ["anymatch@3.1.3", "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
|
||||
|
||||
"aproba": ["aproba@2.0.0", "https://registry.npmmirror.com/aproba/-/aproba-2.0.0.tgz", {}, "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ=="],
|
||||
|
||||
"are-we-there-yet": ["are-we-there-yet@2.0.0", "https://registry.npmmirror.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", { "dependencies": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" } }, "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw=="],
|
||||
|
||||
"arg": ["arg@5.0.2", "https://registry.npmmirror.com/arg/-/arg-5.0.2.tgz", {}, "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg=="],
|
||||
|
||||
"argparse": ["argparse@2.0.1", "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||
@ -656,10 +649,10 @@
|
||||
|
||||
"base64-js": ["base64-js@1.5.1", "https://registry.npmmirror.com/base64-js/-/base64-js-1.5.1.tgz", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
|
||||
|
||||
"bcrypt": ["bcrypt@5.1.1", "https://registry.npmmirror.com/bcrypt/-/bcrypt-5.1.1.tgz", { "dependencies": { "@mapbox/node-pre-gyp": "^1.0.11", "node-addon-api": "^5.0.0" } }, "sha512-AGBHOG5hPYZ5Xl9KXzU5iKq9516yEmvCKDg3ecP5kX2aB6UqTeXZxk2ELnDgDm6BQSMlLt9rDB4LoSMx0rYwww=="],
|
||||
|
||||
"bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "https://registry.npmmirror.com/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="],
|
||||
|
||||
"bcryptjs": ["bcryptjs@3.0.2", "https://registry.npmmirror.com/bcryptjs/-/bcryptjs-3.0.2.tgz", { "bin": { "bcrypt": "bin/bcrypt" } }, "sha512-k38b3XOZKv60C4E2hVsXTolJWfkGRMbILBIe2IBITXciy5bOsTKot5kDrf3ZfufQtQOUN5mXceUEpU1rTl9Uog=="],
|
||||
|
||||
"binary-extensions": ["binary-extensions@2.3.0", "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
|
||||
|
||||
"bl": ["bl@4.1.0", "https://registry.npmmirror.com/bl/-/bl-4.1.0.tgz", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
|
||||
@ -724,8 +717,6 @@
|
||||
|
||||
"color-string": ["color-string@1.9.1", "https://registry.npmmirror.com/color-string/-/color-string-1.9.1.tgz", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
|
||||
|
||||
"color-support": ["color-support@1.1.3", "https://registry.npmmirror.com/color-support/-/color-support-1.1.3.tgz", { "bin": { "color-support": "bin.js" } }, "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="],
|
||||
|
||||
"colorette": ["colorette@2.0.20", "https://registry.npmmirror.com/colorette/-/colorette-2.0.20.tgz", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="],
|
||||
|
||||
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "https://registry.npmmirror.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
|
||||
@ -734,8 +725,6 @@
|
||||
|
||||
"concat-map": ["concat-map@0.0.1", "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||
|
||||
"console-control-strings": ["console-control-strings@1.1.0", "https://registry.npmmirror.com/console-control-strings/-/console-control-strings-1.1.0.tgz", {}, "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="],
|
||||
|
||||
"content-disposition": ["content-disposition@1.0.0", "https://registry.npmmirror.com/content-disposition/-/content-disposition-1.0.0.tgz", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="],
|
||||
|
||||
"content-type": ["content-type@1.0.5", "https://registry.npmmirror.com/content-type/-/content-type-1.0.5.tgz", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
|
||||
@ -778,8 +767,6 @@
|
||||
|
||||
"define-properties": ["define-properties@1.2.1", "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
|
||||
|
||||
"delegates": ["delegates@1.0.0", "https://registry.npmmirror.com/delegates/-/delegates-1.0.0.tgz", {}, "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ=="],
|
||||
|
||||
"depd": ["depd@2.0.0", "https://registry.npmmirror.com/depd/-/depd-2.0.0.tgz", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
|
||||
|
||||
"dequal": ["dequal@2.0.3", "https://registry.npmmirror.com/dequal/-/dequal-2.0.3.tgz", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||
@ -942,6 +929,8 @@
|
||||
|
||||
"flatted": ["flatted@3.3.3", "https://registry.npmmirror.com/flatted/-/flatted-3.3.3.tgz", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
|
||||
|
||||
"flexlayout-react": ["flexlayout-react@0.7.15", "https://registry.npmmirror.com/flexlayout-react/-/flexlayout-react-0.7.15.tgz", { "peerDependencies": { "react": "^18.0.0", "react-dom": "^18.0.0" } }, "sha512-ydTMdEoQO5BniylxVkSxa59rEY0+96lqqRII+QK+yq6028eHywPuxZawt4g45y5pMb9ptP4N9HPAQXAFsxwowQ=="],
|
||||
|
||||
"for-each": ["for-each@0.3.5", "https://registry.npmmirror.com/for-each/-/for-each-0.3.5.tgz", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
|
||||
|
||||
"foreground-child": ["foreground-child@3.3.1", "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
||||
@ -954,10 +943,6 @@
|
||||
|
||||
"fs-constants": ["fs-constants@1.0.0", "https://registry.npmmirror.com/fs-constants/-/fs-constants-1.0.0.tgz", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
|
||||
|
||||
"fs-minipass": ["fs-minipass@2.1.0", "https://registry.npmmirror.com/fs-minipass/-/fs-minipass-2.1.0.tgz", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="],
|
||||
|
||||
"fs.realpath": ["fs.realpath@1.0.0", "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
|
||||
@ -966,8 +951,6 @@
|
||||
|
||||
"functions-have-names": ["functions-have-names@1.2.3", "https://registry.npmmirror.com/functions-have-names/-/functions-have-names-1.2.3.tgz", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="],
|
||||
|
||||
"gauge": ["gauge@3.0.2", "https://registry.npmmirror.com/gauge/-/gauge-3.0.2.tgz", { "dependencies": { "aproba": "^1.0.3 || ^2.0.0", "color-support": "^1.1.2", "console-control-strings": "^1.0.0", "has-unicode": "^2.0.1", "object-assign": "^4.1.1", "signal-exit": "^3.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "wide-align": "^1.1.2" } }, "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q=="],
|
||||
|
||||
"get-caller-file": ["get-caller-file@2.0.5", "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
|
||||
@ -1008,8 +991,6 @@
|
||||
|
||||
"has-tostringtag": ["has-tostringtag@1.0.2", "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
|
||||
|
||||
"has-unicode": ["has-unicode@2.0.1", "https://registry.npmmirror.com/has-unicode/-/has-unicode-2.0.1.tgz", {}, "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="],
|
||||
|
||||
"hasown": ["hasown@2.0.2", "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
|
||||
|
||||
"hast-util-from-dom": ["hast-util-from-dom@5.0.1", "https://registry.npmmirror.com/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz", { "dependencies": { "@types/hast": "^3.0.0", "hastscript": "^9.0.0", "web-namespaces": "^2.0.0" } }, "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q=="],
|
||||
@ -1046,8 +1027,6 @@
|
||||
|
||||
"http-errors": ["http-errors@2.0.0", "https://registry.npmmirror.com/http-errors/-/http-errors-2.0.0.tgz", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
|
||||
|
||||
"https-proxy-agent": ["https-proxy-agent@5.0.1", "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.6.3", "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
|
||||
|
||||
"ieee754": ["ieee754@1.2.1", "https://registry.npmmirror.com/ieee754/-/ieee754-1.2.1.tgz", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
|
||||
@ -1058,8 +1037,6 @@
|
||||
|
||||
"imurmurhash": ["imurmurhash@0.1.4", "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
|
||||
|
||||
"inflight": ["inflight@1.0.6", "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
|
||||
|
||||
"inherits": ["inherits@2.0.4", "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||
|
||||
"inline-style-parser": ["inline-style-parser@0.2.4", "https://registry.npmmirror.com/inline-style-parser/-/inline-style-parser-0.2.4.tgz", {}, "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q=="],
|
||||
@ -1200,8 +1177,6 @@
|
||||
|
||||
"lucide-react": ["lucide-react@0.482.0", "https://registry.npmmirror.com/lucide-react/-/lucide-react-0.482.0.tgz", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-XM8PzHzSrg8ATmmO+fzf+JyYlVVdQnJjuyLDj2p4V2zEtcKeBNAqAoJIGFv1x2HSBa7kT8gpYUxwdQ0g7nypfw=="],
|
||||
|
||||
"make-dir": ["make-dir@3.1.0", "https://registry.npmmirror.com/make-dir/-/make-dir-3.1.0.tgz", { "dependencies": { "semver": "^6.0.0" } }, "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw=="],
|
||||
|
||||
"markdown-extensions": ["markdown-extensions@2.0.0", "https://registry.npmmirror.com/markdown-extensions/-/markdown-extensions-2.0.0.tgz", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="],
|
||||
|
||||
"markdown-table": ["markdown-table@3.0.4", "https://registry.npmmirror.com/markdown-table/-/markdown-table-3.0.4.tgz", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
|
||||
@ -1332,10 +1307,6 @@
|
||||
|
||||
"minipass": ["minipass@7.1.2", "https://registry.npmmirror.com/minipass/-/minipass-7.1.2.tgz", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
|
||||
|
||||
"minizlib": ["minizlib@2.1.2", "https://registry.npmmirror.com/minizlib/-/minizlib-2.1.2.tgz", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="],
|
||||
|
||||
"mkdirp": ["mkdirp@1.0.4", "https://registry.npmmirror.com/mkdirp/-/mkdirp-1.0.4.tgz", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
|
||||
|
||||
"mkdirp-classic": ["mkdirp-classic@0.5.3", "https://registry.npmmirror.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="],
|
||||
|
||||
"monaco-editor": ["monaco-editor@0.36.1", "https://registry.npmmirror.com/monaco-editor/-/monaco-editor-0.36.1.tgz", {}, "sha512-/CaclMHKQ3A6rnzBzOADfwdSJ25BFoFT0Emxsc4zYVyav5SkK9iA6lEtIeuN/oRYbwPgviJT+t3l+sjFa28jYg=="],
|
||||
@ -1370,18 +1341,10 @@
|
||||
|
||||
"next-themes": ["next-themes@0.4.6", "https://registry.npmmirror.com/next-themes/-/next-themes-0.4.6.tgz", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
|
||||
|
||||
"node-addon-api": ["node-addon-api@5.1.0", "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-5.1.0.tgz", {}, "sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA=="],
|
||||
|
||||
"node-fetch": ["node-fetch@2.7.0", "https://registry.npmmirror.com/node-fetch/-/node-fetch-2.7.0.tgz", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||
|
||||
"nopt": ["nopt@5.0.0", "https://registry.npmmirror.com/nopt/-/nopt-5.0.0.tgz", { "dependencies": { "abbrev": "1" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ=="],
|
||||
|
||||
"normalize-path": ["normalize-path@3.0.0", "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||
|
||||
"normalize-url": ["normalize-url@8.0.1", "https://registry.npmmirror.com/normalize-url/-/normalize-url-8.0.1.tgz", {}, "sha512-IO9QvjUMWxPQQhs60oOu10CRkWCiZzSUkzbXGGV9pviYl1fXYcvkzQ5jV9z8Y6un8ARoVRl4EtC6v6jNqbaJ/w=="],
|
||||
|
||||
"npmlog": ["npmlog@5.0.1", "https://registry.npmmirror.com/npmlog/-/npmlog-5.0.1.tgz", { "dependencies": { "are-we-there-yet": "^2.0.0", "console-control-strings": "^1.1.0", "gauge": "^3.0.0", "set-blocking": "^2.0.0" } }, "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw=="],
|
||||
|
||||
"oauth4webapi": ["oauth4webapi@3.5.1", "https://registry.npmmirror.com/oauth4webapi/-/oauth4webapi-3.5.1.tgz", {}, "sha512-txg/jZQwcbaF7PMJgY7aoxc9QuCxHVFMiEkDIJ60DwDz3PbtXPQnrzo+3X4IRYGChIwWLabRBRpf1k9hO9+xrQ=="],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||
@ -1434,8 +1397,6 @@
|
||||
|
||||
"path-exists": ["path-exists@4.0.0", "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
|
||||
|
||||
"path-is-absolute": ["path-is-absolute@1.0.1", "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"path-parse": ["path-parse@1.0.7", "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
|
||||
@ -1590,8 +1551,6 @@
|
||||
|
||||
"reusify": ["reusify@1.1.0", "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="],
|
||||
|
||||
"rimraf": ["rimraf@3.0.2", "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
|
||||
|
||||
"router": ["router@2.2.0", "https://registry.npmmirror.com/router/-/router-2.2.0.tgz", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
|
||||
|
||||
"run-parallel": ["run-parallel@1.2.0", "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
|
||||
@ -1620,8 +1579,6 @@
|
||||
|
||||
"server-only": ["server-only@0.0.1", "https://registry.npmmirror.com/server-only/-/server-only-0.0.1.tgz", {}, "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA=="],
|
||||
|
||||
"set-blocking": ["set-blocking@2.0.0", "https://registry.npmmirror.com/set-blocking/-/set-blocking-2.0.0.tgz", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="],
|
||||
|
||||
"set-function-length": ["set-function-length@1.2.2", "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="],
|
||||
|
||||
"set-function-name": ["set-function-name@2.0.2", "https://registry.npmmirror.com/set-function-name/-/set-function-name-2.0.2.tgz", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="],
|
||||
@ -1724,8 +1681,6 @@
|
||||
|
||||
"tailwindcss-animate": ["tailwindcss-animate@1.0.7", "https://registry.npmmirror.com/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", { "peerDependencies": { "tailwindcss": ">=3.0.0 || insiders" } }, "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA=="],
|
||||
|
||||
"tar": ["tar@6.2.1", "https://registry.npmmirror.com/tar/-/tar-6.2.1.tgz", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="],
|
||||
|
||||
"tar-fs": ["tar-fs@2.1.2", "https://registry.npmmirror.com/tar-fs/-/tar-fs-2.1.2.tgz", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA=="],
|
||||
|
||||
"tar-stream": ["tar-stream@3.1.7", "https://registry.npmmirror.com/tar-stream/-/tar-stream-3.1.7.tgz", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="],
|
||||
@ -1746,8 +1701,6 @@
|
||||
|
||||
"toidentifier": ["toidentifier@1.0.1", "https://registry.npmmirror.com/toidentifier/-/toidentifier-1.0.1.tgz", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
|
||||
|
||||
"tr46": ["tr46@0.0.3", "https://registry.npmmirror.com/tr46/-/tr46-0.0.3.tgz", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||
|
||||
"trim-lines": ["trim-lines@3.0.1", "https://registry.npmmirror.com/trim-lines/-/trim-lines-3.0.1.tgz", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
|
||||
|
||||
"trough": ["trough@2.2.0", "https://registry.npmmirror.com/trough/-/trough-2.2.0.tgz", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
|
||||
@ -1848,10 +1801,6 @@
|
||||
|
||||
"web-namespaces": ["web-namespaces@2.0.1", "https://registry.npmmirror.com/web-namespaces/-/web-namespaces-2.0.1.tgz", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="],
|
||||
|
||||
"webidl-conversions": ["webidl-conversions@3.0.1", "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||
|
||||
"whatwg-url": ["whatwg-url@5.0.0", "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-5.0.0.tgz", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||
|
||||
"which": ["which@2.0.2", "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"which-boxed-primitive": ["which-boxed-primitive@1.1.1", "https://registry.npmmirror.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
|
||||
@ -1862,8 +1811,6 @@
|
||||
|
||||
"which-typed-array": ["which-typed-array@1.1.19", "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.19.tgz", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="],
|
||||
|
||||
"wide-align": ["wide-align@1.1.5", "https://registry.npmmirror.com/wide-align/-/wide-align-1.1.5.tgz", { "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } }, "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg=="],
|
||||
|
||||
"word-wrap": ["word-wrap@1.2.5", "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
|
||||
|
||||
"wrap-ansi": ["wrap-ansi@7.0.0", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||
@ -1874,8 +1821,6 @@
|
||||
|
||||
"y18n": ["y18n@5.0.8", "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
|
||||
|
||||
"yallist": ["yallist@4.0.0", "https://registry.npmmirror.com/yallist/-/yallist-4.0.0.tgz", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
|
||||
|
||||
"yaml": ["yaml@2.7.1", "https://registry.npmmirror.com/yaml/-/yaml-2.7.1.tgz", { "bin": { "yaml": "bin.mjs" } }, "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ=="],
|
||||
|
||||
"yargs": ["yargs@17.7.2", "https://registry.npmmirror.com/yargs/-/yargs-17.7.2.tgz", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
|
||||
@ -1928,38 +1873,24 @@
|
||||
|
||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
|
||||
|
||||
"fs-minipass/minipass": ["minipass@3.3.6", "https://registry.npmmirror.com/minipass/-/minipass-3.3.6.tgz", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||
|
||||
"gauge/signal-exit": ["signal-exit@3.0.7", "https://registry.npmmirror.com/signal-exit/-/signal-exit-3.0.7.tgz", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||
|
||||
"glob/minimatch": ["minimatch@9.0.5", "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
|
||||
|
||||
"jsondiffpatch/chalk": ["chalk@5.4.1", "https://registry.npmmirror.com/chalk/-/chalk-5.4.1.tgz", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
|
||||
|
||||
"katex/commander": ["commander@8.3.0", "https://registry.npmmirror.com/commander/-/commander-8.3.0.tgz", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="],
|
||||
|
||||
"make-dir/semver": ["semver@6.3.1", "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
|
||||
|
||||
"mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
|
||||
|
||||
"minizlib/minipass": ["minipass@3.3.6", "https://registry.npmmirror.com/minipass/-/minipass-3.3.6.tgz", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
|
||||
|
||||
"monaco-languageclient/vscode-languageclient": ["vscode-languageclient@8.1.0", "https://registry.npmmirror.com/vscode-languageclient/-/vscode-languageclient-8.1.0.tgz", { "dependencies": { "minimatch": "^5.1.0", "semver": "^7.3.7", "vscode-languageserver-protocol": "3.17.3" } }, "sha512-GL4QdbYUF/XxQlAsvYWZRV3V34kOkpRlvV60/72ghHfsYFnS/v2MANZ9P6sHmxFcZKOse8O+L9G7Czg0NUWing=="],
|
||||
|
||||
"next/postcss": ["postcss@8.4.31", "https://registry.npmmirror.com/postcss/-/postcss-8.4.31.tgz", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
|
||||
|
||||
"parse-entities/@types/unist": ["@types/unist@2.0.11", "https://registry.npmmirror.com/@types/unist/-/unist-2.0.11.tgz", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
|
||||
|
||||
"rimraf/glob": ["glob@7.2.3", "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
|
||||
|
||||
"string-width/emoji-regex": ["emoji-regex@8.0.0", "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
|
||||
|
||||
"tar/chownr": ["chownr@2.0.0", "https://registry.npmmirror.com/chownr/-/chownr-2.0.0.tgz", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
|
||||
|
||||
"tar/minipass": ["minipass@5.0.0", "https://registry.npmmirror.com/minipass/-/minipass-5.0.0.tgz", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="],
|
||||
|
||||
"tar-fs/tar-stream": ["tar-stream@2.2.0", "https://registry.npmmirror.com/tar-stream/-/tar-stream-2.2.0.tgz", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
|
||||
|
||||
"tinyglobby/picomatch": ["picomatch@4.0.2", "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.2.tgz", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
|
||||
|
@ -49,7 +49,7 @@
|
||||
"DetailsPage": {
|
||||
"BackButton": "All Submissions",
|
||||
"Time": "Submitted on",
|
||||
"Input": "Input",
|
||||
"Input": "Last Executed Input",
|
||||
"ExpectedOutput": "Expected Output",
|
||||
"ActualOutput": "Acutal Output",
|
||||
"Code": "Code"
|
||||
@ -127,7 +127,8 @@
|
||||
"description": "Enter your email below to sign up to your account",
|
||||
"or": "Or",
|
||||
"haveAccount": "Already have an account?",
|
||||
"signIn": "Sign in"
|
||||
"signIn": "Sign in",
|
||||
"oauth": "Sign in with {provider}"
|
||||
},
|
||||
"StatusMessage": {
|
||||
"PD": "Pending",
|
||||
@ -150,12 +151,25 @@
|
||||
"Time": "Time",
|
||||
"Memory": "Memory"
|
||||
},
|
||||
"Testcase": {
|
||||
"Table": {
|
||||
"Case": "Case"
|
||||
}
|
||||
},
|
||||
"WorkspaceEditorHeader": {
|
||||
"LspStatusButton": {
|
||||
"TooltipContent": "Language Server"
|
||||
},
|
||||
"AnalyzeButton": {
|
||||
"TooltipContent": "Analyze",
|
||||
"ComplexityAnalysis": "Complexity Analysis",
|
||||
"TimeComplexity": "Time Complexity:",
|
||||
"SpaceComplexity": "Space Complexity",
|
||||
"Error": "Error occurred while analyzing complexity, please try again later.",
|
||||
"Analyzing": "Analyzing..."
|
||||
},
|
||||
"ResetButton": {
|
||||
"TooltipContent": "Reset Code"
|
||||
"TooltipContent": "Reset"
|
||||
},
|
||||
"UndoButton": {
|
||||
"TooltipContent": "Undo"
|
||||
@ -215,5 +229,13 @@
|
||||
"answer4": "Editor uses @shikijs/monaco themes, documentation rendered with github-markdown-css"
|
||||
}
|
||||
}
|
||||
},
|
||||
"LoginPromptCard": {
|
||||
"title": "Join Judge4c to Code!",
|
||||
"description": "View your Submission records here",
|
||||
"loginButton": "Log In"
|
||||
},
|
||||
"Video": {
|
||||
"unsupportedBrowser": "Your browser does not support HTML5 video."
|
||||
}
|
||||
}
|
||||
|
@ -17,13 +17,13 @@
|
||||
},
|
||||
"BackButton": "返回",
|
||||
"Bot": {
|
||||
"title": "询问AI助手",
|
||||
"description": "由Vercel Ai SDK驱动",
|
||||
"placeholder": "AI助手将自动获取您当前的代码"
|
||||
"title": "询问 AI 助手",
|
||||
"description": "由 Vercel Ai SDK 驱动",
|
||||
"placeholder": "AI 助手将自动获取您当前的代码"
|
||||
},
|
||||
"BotVisibilityToggle": {
|
||||
"open": "打开AI助手",
|
||||
"close": "关闭AI助手"
|
||||
"open": "打开 AI 助手",
|
||||
"close": "关闭 AI 助手"
|
||||
},
|
||||
"CredentialsSignInForm": {
|
||||
"email": "邮箱",
|
||||
@ -49,7 +49,7 @@
|
||||
"DetailsPage": {
|
||||
"BackButton": "所有提交记录",
|
||||
"Time": "提交于",
|
||||
"Input": "输入",
|
||||
"Input": "最后执行的输入",
|
||||
"ExpectedOutput": "期望输出",
|
||||
"ActualOutput": "实际输出",
|
||||
"Code": "代码"
|
||||
@ -127,7 +127,8 @@
|
||||
"description": "请输入你的邮箱以注册账户",
|
||||
"or": "或者",
|
||||
"haveAccount": "已经有账户了?",
|
||||
"signIn": "登录"
|
||||
"signIn": "登录",
|
||||
"oauth": "使用 {provider} 登录"
|
||||
},
|
||||
"StatusMessage": {
|
||||
"PD": "待处理",
|
||||
@ -150,12 +151,25 @@
|
||||
"Time": "执行用时",
|
||||
"Memory": "消耗内存"
|
||||
},
|
||||
"Testcase": {
|
||||
"Table": {
|
||||
"Case": "样例"
|
||||
}
|
||||
},
|
||||
"WorkspaceEditorHeader": {
|
||||
"LspStatusButton": {
|
||||
"TooltipContent": "语言服务"
|
||||
},
|
||||
"AnalyzeButton": {
|
||||
"TooltipContent": "分析",
|
||||
"ComplexityAnalysis": "复杂度分析",
|
||||
"TimeComplexity": "时间复杂度:",
|
||||
"SpaceComplexity": "空间复杂度:",
|
||||
"Error": "解析复杂度时出错,请稍后重试。",
|
||||
"Analyzing": "分析中..."
|
||||
},
|
||||
"ResetButton": {
|
||||
"TooltipContent": "重置代码"
|
||||
"TooltipContent": "重置"
|
||||
},
|
||||
"UndoButton": {
|
||||
"TooltipContent": "撤销"
|
||||
@ -215,5 +229,13 @@
|
||||
"answer4": "编辑器采用 @shikijs/monaco, 文档采用 github-markdown-css 样式"
|
||||
}
|
||||
}
|
||||
},
|
||||
"LoginPromptCard": {
|
||||
"title": "加入 Judge4c 开始编程!",
|
||||
"description": "在此查看您的提交记录",
|
||||
"loginButton": "登录"
|
||||
},
|
||||
"Video": {
|
||||
"unsupportedBrowser": "您的浏览器不支持 HTML5 视频。"
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@
|
||||
"seed": "tsx prisma/seed.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/deepseek": "^0.2.14",
|
||||
"@ai-sdk/openai": "^1.3.0",
|
||||
"@ai-sdk/react": "^1.2.0",
|
||||
"@auth/prisma-adapter": "^2.8.0",
|
||||
@ -41,13 +42,14 @@
|
||||
"@tanstack/react-table": "^8.21.2",
|
||||
"@types/vscode": "^1.97.0",
|
||||
"ai": "^4.2.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"devicons-react": "^1.4.0",
|
||||
"dockerode": "^4.0.4",
|
||||
"dockview": "^4.2.1",
|
||||
"flexlayout-react": "^0.7.15",
|
||||
"framer-motion": "^12.7.3",
|
||||
"github-markdown-css": "^5.8.1",
|
||||
"lucide-react": "^0.482.0",
|
||||
@ -82,7 +84,6 @@
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@shikijs/monaco": "^3.2.1",
|
||||
"@types/bcrypt": "^5.0.2",
|
||||
"@types/dockerode": "^3.3.35",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
|
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "password" TEXT;
|
@ -59,6 +59,7 @@ model User {
|
||||
id String @id @default(cuid())
|
||||
name String?
|
||||
email String @unique
|
||||
password String?
|
||||
emailVerified DateTime?
|
||||
image String?
|
||||
role Role @default(GUEST)
|
||||
|
986
prisma/seed.ts
986
prisma/seed.ts
File diff suppressed because it is too large
Load Diff
BIN
public/sign-in.mp4
Normal file
BIN
public/sign-in.mp4
Normal file
Binary file not shown.
@ -1,544 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import fs from "fs";
|
||||
import tar from "tar-stream";
|
||||
import Docker from "dockerode";
|
||||
import prisma from "@/lib/prisma";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { redirect } from "next/navigation";
|
||||
import { Readable, Writable } from "stream";
|
||||
import { Status } from "@/generated/client";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import type { ProblemWithTestcases, TestcaseWithDetails } from "@/types/prisma";
|
||||
import type { EditorLanguage, Submission, TestcaseResult } from "@/generated/client";
|
||||
|
||||
const isRemote = process.env.DOCKER_HOST_MODE === "remote";
|
||||
|
||||
// Docker client initialization
|
||||
const docker = isRemote
|
||||
? new Docker({
|
||||
protocol: process.env.DOCKER_REMOTE_PROTOCOL as "https" | "http" | "ssh" | undefined,
|
||||
host: process.env.DOCKER_REMOTE_HOST,
|
||||
port: process.env.DOCKER_REMOTE_PORT,
|
||||
ca: fs.readFileSync(process.env.DOCKER_REMOTE_CA_PATH || "/certs/ca.pem"),
|
||||
cert: fs.readFileSync(process.env.DOCKER_REMOTE_CERT_PATH || "/certs/cert.pem"),
|
||||
key: fs.readFileSync(process.env.DOCKER_REMOTE_KEY_PATH || "/certs/key.pem"),
|
||||
})
|
||||
: new Docker({ socketPath: "/var/run/docker.sock" });
|
||||
|
||||
// Prepare Docker image environment
|
||||
async function prepareEnvironment(image: string, tag: string): Promise<boolean> {
|
||||
try {
|
||||
const reference = `${image}:${tag}`;
|
||||
const filters = { reference: [reference] };
|
||||
const images = await docker.listImages({ filters });
|
||||
return images.length !== 0;
|
||||
} catch (error) {
|
||||
console.error("Error checking Docker images:", error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Create Docker container with keep-alive
|
||||
async function createContainer(
|
||||
image: string,
|
||||
tag: string,
|
||||
workingDir: string,
|
||||
memoryLimit?: number
|
||||
) {
|
||||
const container = await docker.createContainer({
|
||||
Image: `${image}:${tag}`,
|
||||
Cmd: ["tail", "-f", "/dev/null"], // Keep container alive
|
||||
WorkingDir: workingDir,
|
||||
HostConfig: {
|
||||
Memory: memoryLimit ? memoryLimit * 1024 * 1024 : undefined,
|
||||
MemorySwap: memoryLimit ? memoryLimit * 1024 * 1024 : undefined,
|
||||
},
|
||||
NetworkDisabled: true,
|
||||
});
|
||||
|
||||
await container.start();
|
||||
return container;
|
||||
}
|
||||
|
||||
// Create tar stream for code submission
|
||||
function createTarStream(file: string, value: string) {
|
||||
const pack = tar.pack();
|
||||
pack.entry({ name: file }, value);
|
||||
pack.finalize();
|
||||
return Readable.from(pack);
|
||||
}
|
||||
|
||||
export async function judge(
|
||||
language: EditorLanguage,
|
||||
code: string,
|
||||
problemId: string,
|
||||
): Promise<Submission> {
|
||||
const session = await auth();
|
||||
if (!session?.user?.id) redirect("/sign-in");
|
||||
|
||||
const userId = session.user.id;
|
||||
let container: Docker.Container | null = null;
|
||||
let submission: Submission | null = null;
|
||||
|
||||
try {
|
||||
const problem = await prisma.problem.findUnique({
|
||||
where: { id: problemId },
|
||||
include: {
|
||||
testcases: {
|
||||
include: {
|
||||
data: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}) as ProblemWithTestcases | null;
|
||||
|
||||
if (!problem) {
|
||||
submission = await prisma.submission.create({
|
||||
data: {
|
||||
language,
|
||||
code,
|
||||
status: Status.SE,
|
||||
userId,
|
||||
problemId,
|
||||
message: "Problem not found",
|
||||
},
|
||||
});
|
||||
return submission;
|
||||
}
|
||||
|
||||
const config = await prisma.editorLanguageConfig.findUnique({
|
||||
where: { language },
|
||||
include: {
|
||||
dockerConfig: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!config?.dockerConfig) {
|
||||
submission = await prisma.submission.create({
|
||||
data: {
|
||||
language,
|
||||
code,
|
||||
status: Status.SE,
|
||||
userId,
|
||||
problemId,
|
||||
message: " Missing editor or docker configuration",
|
||||
},
|
||||
});
|
||||
return submission;
|
||||
}
|
||||
|
||||
const testcases = problem.testcases;
|
||||
|
||||
if (!testcases || testcases.length === 0) {
|
||||
submission = await prisma.submission.create({
|
||||
data: {
|
||||
language,
|
||||
code,
|
||||
status: Status.SE,
|
||||
userId,
|
||||
problemId,
|
||||
message: "Testcases not found",
|
||||
},
|
||||
});
|
||||
return submission;
|
||||
}
|
||||
|
||||
const {
|
||||
image,
|
||||
tag,
|
||||
workingDir,
|
||||
compileOutputLimit,
|
||||
runOutputLimit,
|
||||
} = config.dockerConfig;
|
||||
const { fileName, fileExtension } = config;
|
||||
const file = `${fileName}.${fileExtension}`;
|
||||
|
||||
// Prepare the environment and create a container
|
||||
if (await prepareEnvironment(image, tag)) {
|
||||
container = await createContainer(image, tag, workingDir, problem.memoryLimit);
|
||||
} else {
|
||||
console.error("Docker image not found:", image, ":", tag);
|
||||
submission = await prisma.submission.create({
|
||||
data: {
|
||||
language,
|
||||
code,
|
||||
status: Status.SE,
|
||||
userId,
|
||||
problemId,
|
||||
message: "The docker environment is not ready",
|
||||
},
|
||||
});
|
||||
return submission;
|
||||
}
|
||||
|
||||
submission = await prisma.submission.create({
|
||||
data: {
|
||||
language,
|
||||
code,
|
||||
status: Status.PD,
|
||||
userId,
|
||||
problemId,
|
||||
message: "",
|
||||
},
|
||||
});
|
||||
|
||||
// Upload code to the container
|
||||
const tarStream = createTarStream(file, code);
|
||||
await container.putArchive(tarStream, { path: workingDir });
|
||||
|
||||
// Compile the code
|
||||
const compileResult = await compile(container, file, fileName, compileOutputLimit, submission.id, language);
|
||||
if (compileResult.status === Status.CE) {
|
||||
return compileResult;
|
||||
}
|
||||
|
||||
// Run the code
|
||||
const runResult = await run(container, fileName, problem.timeLimit, runOutputLimit, submission.id, testcases);
|
||||
return runResult;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (submission) {
|
||||
const updatedSubmission = await prisma.submission.update({
|
||||
where: { id: submission.id },
|
||||
data: {
|
||||
status: Status.SE,
|
||||
message: "System Error",
|
||||
}
|
||||
})
|
||||
return updatedSubmission;
|
||||
} else {
|
||||
submission = await prisma.submission.create({
|
||||
data: {
|
||||
language,
|
||||
code,
|
||||
status: Status.PD,
|
||||
userId,
|
||||
problemId,
|
||||
message: "",
|
||||
},
|
||||
})
|
||||
return submission;
|
||||
}
|
||||
} finally {
|
||||
revalidatePath(`/problems/${problemId}`);
|
||||
if (container) {
|
||||
try {
|
||||
await container.kill();
|
||||
await container.remove();
|
||||
} catch (error) {
|
||||
console.error("Container cleanup failed:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function compile(
|
||||
container: Docker.Container,
|
||||
file: string,
|
||||
fileName: string,
|
||||
compileOutputLimit: number = 1 * 1024 * 1024,
|
||||
submissionId: string,
|
||||
language: EditorLanguage,
|
||||
): Promise<Submission> {
|
||||
const compileCmd =
|
||||
language === "c"
|
||||
? ["gcc", "-O2", file, "-o", fileName]
|
||||
: language === "cpp"
|
||||
? ["g++", "-O2", file, "-o", fileName]
|
||||
: null;
|
||||
|
||||
if (!compileCmd) {
|
||||
return prisma.submission.update({
|
||||
where: { id: submissionId },
|
||||
data: {
|
||||
status: Status.SE,
|
||||
message: "Unsupported language",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const compileExec = await container.exec({
|
||||
Cmd: compileCmd,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
});
|
||||
|
||||
return new Promise<Submission>((resolve, reject) => {
|
||||
compileExec.start({}, (error, stream) => {
|
||||
if (error || !stream) {
|
||||
return reject({ message: "System Error", Status: Status.SE });
|
||||
}
|
||||
|
||||
const stdoutChunks: string[] = [];
|
||||
let stdoutLength = 0;
|
||||
const stdoutStream = new Writable({
|
||||
write(chunk, _encoding, callback) {
|
||||
let text = chunk.toString();
|
||||
if (stdoutLength + text.length > compileOutputLimit) {
|
||||
text = text.substring(0, compileOutputLimit - stdoutLength);
|
||||
stdoutChunks.push(text);
|
||||
stdoutLength = compileOutputLimit;
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
stdoutChunks.push(text);
|
||||
stdoutLength += text.length;
|
||||
callback();
|
||||
},
|
||||
});
|
||||
|
||||
const stderrChunks: string[] = [];
|
||||
let stderrLength = 0;
|
||||
const stderrStream = new Writable({
|
||||
write(chunk, _encoding, callback) {
|
||||
let text = chunk.toString();
|
||||
if (stderrLength + text.length > compileOutputLimit) {
|
||||
text = text.substring(0, compileOutputLimit - stderrLength);
|
||||
stderrChunks.push(text);
|
||||
stderrLength = compileOutputLimit;
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
stderrChunks.push(text);
|
||||
stderrLength += text.length;
|
||||
callback();
|
||||
},
|
||||
});
|
||||
|
||||
docker.modem.demuxStream(stream, stdoutStream, stderrStream);
|
||||
|
||||
stream.on("end", async () => {
|
||||
const stdout = stdoutChunks.join("");
|
||||
const stderr = stderrChunks.join("");
|
||||
const exitCode = (await compileExec.inspect()).ExitCode;
|
||||
|
||||
let updatedSubmission: Submission;
|
||||
|
||||
if (exitCode !== 0 || stderr) {
|
||||
updatedSubmission = await prisma.submission.update({
|
||||
where: { id: submissionId },
|
||||
data: {
|
||||
status: Status.CE,
|
||||
message: stderr || "Compilation Error",
|
||||
},
|
||||
});
|
||||
} else {
|
||||
updatedSubmission = await prisma.submission.update({
|
||||
where: { id: submissionId },
|
||||
data: {
|
||||
status: Status.CS,
|
||||
message: stdout,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
resolve(updatedSubmission);
|
||||
});
|
||||
|
||||
stream.on("error", () => {
|
||||
reject({ message: "System Error", Status: Status.SE });
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Run code and implement timeout
|
||||
async function run(
|
||||
container: Docker.Container,
|
||||
fileName: string,
|
||||
timeLimit: number = 1000,
|
||||
maxOutput: number = 1 * 1024 * 1024,
|
||||
submissionId: string,
|
||||
testcases: TestcaseWithDetails,
|
||||
): Promise<Submission> {
|
||||
let finalSubmission: Submission | null = null;
|
||||
let maxExecutionTime = 0;
|
||||
|
||||
for (const testcase of testcases) {
|
||||
const sortedData = testcase.data.sort((a, b) => a.index - b.index);
|
||||
const inputData = sortedData.map(d => d.value).join("\n");
|
||||
|
||||
const runExec = await container.exec({
|
||||
Cmd: [`./${fileName}`],
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
AttachStdin: true,
|
||||
});
|
||||
|
||||
const result = await new Promise<Submission | TestcaseResult>((resolve, reject) => {
|
||||
// Start the exec stream
|
||||
runExec.start({ hijack: true }, async (error, stream) => {
|
||||
if (error || !stream) {
|
||||
const submission = await prisma.submission.update({
|
||||
where: { id: submissionId },
|
||||
data: {
|
||||
status: Status.SE,
|
||||
message: "System Error",
|
||||
}
|
||||
})
|
||||
return resolve(submission);
|
||||
}
|
||||
|
||||
stream.write(inputData);
|
||||
stream.end();
|
||||
|
||||
const stdoutChunks: string[] = [];
|
||||
const stderrChunks: string[] = [];
|
||||
let stdoutLength = 0;
|
||||
let stderrLength = 0;
|
||||
|
||||
const stdoutStream = new Writable({
|
||||
write: (chunk, _, callback) => {
|
||||
const text = chunk.toString();
|
||||
if (stdoutLength + text.length > maxOutput) {
|
||||
stdoutChunks.push(text.substring(0, maxOutput - stdoutLength));
|
||||
stdoutLength = maxOutput;
|
||||
} else {
|
||||
stdoutChunks.push(text);
|
||||
stdoutLength += text.length;
|
||||
}
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
const stderrStream = new Writable({
|
||||
write: (chunk, _, callback) => {
|
||||
const text = chunk.toString();
|
||||
if (stderrLength + text.length > maxOutput) {
|
||||
stderrChunks.push(text.substring(0, maxOutput - stderrLength));
|
||||
stderrLength = maxOutput;
|
||||
} else {
|
||||
stderrChunks.push(text);
|
||||
stderrLength += text.length;
|
||||
}
|
||||
callback();
|
||||
}
|
||||
});
|
||||
|
||||
docker.modem.demuxStream(stream, stdoutStream, stderrStream);
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
// Timeout mechanism
|
||||
const timeoutId = setTimeout(async () => {
|
||||
stream.destroy(); // Destroy the stream to stop execution
|
||||
await prisma.testcaseResult.create({
|
||||
data: {
|
||||
isCorrect: false,
|
||||
output: "",
|
||||
submissionId,
|
||||
testcaseId: testcase.id,
|
||||
}
|
||||
})
|
||||
const updatedSubmission = await prisma.submission.update({
|
||||
where: { id: submissionId },
|
||||
data: {
|
||||
status: Status.TLE,
|
||||
message: "Time Limit Exceeded",
|
||||
}
|
||||
})
|
||||
resolve(updatedSubmission);
|
||||
}, timeLimit);
|
||||
|
||||
stream.on("end", async () => {
|
||||
clearTimeout(timeoutId); // Clear the timeout if the program finishes before the time limit
|
||||
const stdout = stdoutChunks.join("");
|
||||
const stderr = stderrChunks.join("");
|
||||
const exitCode = (await runExec.inspect()).ExitCode;
|
||||
const executionTime = Date.now() - startTime;
|
||||
|
||||
// Exit code 0 means successful execution
|
||||
if (exitCode === 0) {
|
||||
const expectedOutput = testcase.expectedOutput;
|
||||
const testcaseResult = await prisma.testcaseResult.create({
|
||||
data: {
|
||||
isCorrect: stdout.trim() === expectedOutput.trim(),
|
||||
output: stdout,
|
||||
executionTime,
|
||||
submissionId,
|
||||
testcaseId: testcase.id,
|
||||
}
|
||||
})
|
||||
resolve(testcaseResult);
|
||||
} else if (exitCode === 137) {
|
||||
await prisma.testcaseResult.create({
|
||||
data: {
|
||||
isCorrect: false,
|
||||
output: stdout,
|
||||
executionTime,
|
||||
submissionId,
|
||||
testcaseId: testcase.id,
|
||||
}
|
||||
})
|
||||
const updatedSubmission = await prisma.submission.update({
|
||||
where: { id: submissionId },
|
||||
data: {
|
||||
status: Status.MLE,
|
||||
message: stderr || "Memory Limit Exceeded",
|
||||
}
|
||||
})
|
||||
resolve(updatedSubmission);
|
||||
} else {
|
||||
await prisma.testcaseResult.create({
|
||||
data: {
|
||||
isCorrect: false,
|
||||
output: stdout,
|
||||
executionTime,
|
||||
submissionId,
|
||||
testcaseId: testcase.id,
|
||||
}
|
||||
})
|
||||
const updatedSubmission = await prisma.submission.update({
|
||||
where: { id: submissionId },
|
||||
data: {
|
||||
status: Status.RE,
|
||||
message: stderr || "Runtime Error",
|
||||
}
|
||||
})
|
||||
resolve(updatedSubmission);
|
||||
}
|
||||
});
|
||||
|
||||
stream.on("error", () => {
|
||||
clearTimeout(timeoutId); // Clear timeout in case of error
|
||||
reject({ message: "System Error", Status: Status.SE });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
if ('status' in result) {
|
||||
return result;
|
||||
} else {
|
||||
if (!result.isCorrect) {
|
||||
finalSubmission = await prisma.submission.update({
|
||||
where: { id: submissionId },
|
||||
data: {
|
||||
status: Status.WA,
|
||||
message: "Wrong Answer",
|
||||
},
|
||||
include: {
|
||||
testcaseResults: true,
|
||||
}
|
||||
});
|
||||
return finalSubmission;
|
||||
} else {
|
||||
maxExecutionTime = Math.max(maxExecutionTime, result.executionTime ?? 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
const maxMemoryUsage = (await container.stats({ stream: false, "one-shot": true })).memory_stats.max_usage;
|
||||
finalSubmission = await prisma.submission.update({
|
||||
where: { id: submissionId },
|
||||
data: {
|
||||
status: Status.AC,
|
||||
message: "All testcases passed",
|
||||
executionTime: maxExecutionTime,
|
||||
memoryUsage: maxMemoryUsage / 1024 / 1024,
|
||||
},
|
||||
include: {
|
||||
testcaseResults: true,
|
||||
}
|
||||
});
|
||||
return finalSubmission;
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
"use server";
|
||||
|
||||
import prisma from "@/lib/prisma";
|
||||
import { EditorLanguage } from "@/generated/client";
|
||||
import { SettingsLanguageServerFormValues } from "@/app/(app)/dashboard/@admin/settings/language-server/form";
|
||||
|
||||
export const getLanguageServerConfig = async (language: EditorLanguage) => {
|
||||
return await prisma.languageServerConfig.findUnique({
|
||||
where: { language },
|
||||
});
|
||||
};
|
||||
|
||||
export const handleLanguageServerConfigSubmit = async (
|
||||
language: EditorLanguage,
|
||||
data: SettingsLanguageServerFormValues
|
||||
) => {
|
||||
const existing = await getLanguageServerConfig(language);
|
||||
|
||||
if (existing) {
|
||||
await prisma.languageServerConfig.update({
|
||||
where: { language },
|
||||
data,
|
||||
});
|
||||
} else {
|
||||
await prisma.languageServerConfig.create({
|
||||
data: { ...data, language },
|
||||
});
|
||||
}
|
||||
};
|
@ -1,50 +0,0 @@
|
||||
import {
|
||||
SidebarInset,
|
||||
SidebarProvider,
|
||||
SidebarTrigger,
|
||||
} from "@/components/ui/sidebar";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { User } from "@/generated/client";
|
||||
import { redirect } from "next/navigation";
|
||||
import { Navbar } from "@/components/navbar";
|
||||
import { AppSidebar } from "@/components/app-sidebar";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { type NavUserProps } from "@/components/nav-user";
|
||||
|
||||
interface AdminDashboardLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default async function AdminDashboardLayout({
|
||||
children,
|
||||
}: AdminDashboardLayoutProps) {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user) {
|
||||
redirect("/sign-in");
|
||||
}
|
||||
|
||||
const user: NavUserProps["user"] = (({ name, email, image }) => ({
|
||||
name: name ?? "",
|
||||
email: email ?? "",
|
||||
avatar: image ?? "",
|
||||
}))(session.user as User);
|
||||
|
||||
return (
|
||||
<SidebarProvider>
|
||||
<AppSidebar user={user} />
|
||||
<SidebarInset>
|
||||
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
|
||||
<div className="flex items-center gap-2 px-4">
|
||||
<SidebarTrigger className="-ml-1" />
|
||||
<Separator orientation="vertical" className="mr-2 h-4" />
|
||||
<Navbar />
|
||||
</div>
|
||||
</header>
|
||||
<div className="flex flex-1 flex-col gap-4 p-4 pt-0">
|
||||
{children}
|
||||
</div>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
);
|
||||
}
|
@ -1,3 +0,0 @@
|
||||
export default function DashboardAdmin() {
|
||||
return <div>Dashboard Admin</div>;
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import NewProblemDescriptionForm from "@/components/features/dashboard/admin/problemset/new/components/description-form";
|
||||
|
||||
export default function NewProblemDescriptionPage() {
|
||||
return <NewProblemDescriptionForm />;
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import NewProblemMetadataForm from "@/components/features/dashboard/admin/problemset/new/components/metadata-form";
|
||||
|
||||
export default function NewProblemMetadataPage() {
|
||||
return <NewProblemMetadataForm />;
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function NewProblemPage() {
|
||||
redirect("/dashboard/problemset/new/metadata");
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
import NewProblemSolutionForm from "@/components/features/dashboard/admin/problemset/new/components/solution-form";
|
||||
|
||||
export default function NewProblemSolutionPage() {
|
||||
return <NewProblemSolutionForm />;
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
import { create } from "zustand";
|
||||
import { createJSONStorage, persist } from "zustand/middleware";
|
||||
import { ProblemSchema } from "@/components/features/dashboard/admin/problemset/new/schema";
|
||||
|
||||
interface NewProblemActions {
|
||||
setHydrated: (value: boolean) => void;
|
||||
setData: (data: Partial<ProblemSchema>) => void;
|
||||
}
|
||||
|
||||
type NewProblemState = Partial<ProblemSchema> & {
|
||||
hydrated: boolean;
|
||||
} & NewProblemActions;
|
||||
|
||||
export const useNewProblemStore = create<NewProblemState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
hydrated: false,
|
||||
setHydrated: (value) => set({ hydrated: value }),
|
||||
setData: (data) => set(data),
|
||||
}),
|
||||
{
|
||||
name: "zustand:new-problem",
|
||||
storage: createJSONStorage(() => localStorage),
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
partialize: ({ hydrated, ...rest }) => rest,
|
||||
onRehydrateStorage: () => (state, error) => {
|
||||
if (error) {
|
||||
console.error("An error happened during hydration", error);
|
||||
} else if (state) {
|
||||
state.setHydrated(true);
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
);
|
@ -1,19 +0,0 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { ProblemsetTable } from "@/components/features/dashboard/admin/problemset/table";
|
||||
|
||||
export default async function AdminDashboardProblemsetPage() {
|
||||
const problems = await prisma.problem.findMany({
|
||||
select: {
|
||||
id: true,
|
||||
displayId: true,
|
||||
title: true,
|
||||
difficulty: true,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="h-full px-4">
|
||||
<ProblemsetTable data={problems} />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,77 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Loading } from "@/components/loading";
|
||||
import { useAdminSettingsStore } from "@/stores/useAdminSettingsStore";
|
||||
import { EditorLanguage, LanguageServerConfig } from "@/generated/client";
|
||||
import { SettingsLanguageServerForm } from "@/app/(app)/dashboard/@admin/settings/language-server/form";
|
||||
|
||||
interface LanguageServerAccordionProps {
|
||||
configs: {
|
||||
language: EditorLanguage;
|
||||
config: LanguageServerConfig | null;
|
||||
}[];
|
||||
}
|
||||
|
||||
export function LanguageServerAccordion({
|
||||
configs,
|
||||
}: LanguageServerAccordionProps) {
|
||||
const { hydrated, activeLanguageServerSetting, setActiveLanguageServerSetting } =
|
||||
useAdminSettingsStore();
|
||||
|
||||
if (!hydrated) {
|
||||
return (
|
||||
<div className="h-full w-full space-y-2">
|
||||
<Loading className="h-12 p-0" skeletonClassName="rounded-md" />
|
||||
<Loading className="h-12 p-0" skeletonClassName="rounded-md" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
className="w-full space-y-2"
|
||||
value={activeLanguageServerSetting}
|
||||
onValueChange={setActiveLanguageServerSetting}
|
||||
>
|
||||
{configs.map(({ language, config }) => (
|
||||
<AccordionItem
|
||||
key={language}
|
||||
value={language}
|
||||
className="has-focus-visible:border-ring has-focus-visible:ring-ring/50 rounded-md border outline-none last:border-b has-focus-visible:ring-[3px]"
|
||||
>
|
||||
<AccordionTrigger className="px-4 py-3 justify-start gap-3 text-[15px] leading-6 hover:no-underline focus-visible:ring-0 [&>svg]:-order-1">
|
||||
{language.toUpperCase()}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-muted-foreground pb-0">
|
||||
<div className="px-4 py-3">
|
||||
<SettingsLanguageServerForm
|
||||
defaultValues={
|
||||
config
|
||||
? {
|
||||
protocol: config.protocol,
|
||||
hostname: config.hostname,
|
||||
port: config.port,
|
||||
path: config.path,
|
||||
}
|
||||
: {
|
||||
port: null,
|
||||
path: null,
|
||||
}
|
||||
}
|
||||
language={language}
|
||||
/>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
))}
|
||||
</Accordion>
|
||||
);
|
||||
}
|
@ -1,181 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { z } from "zod";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { EditorLanguage, LanguageServerProtocol } from "@/generated/client";
|
||||
import { handleLanguageServerConfigSubmit } from "@/actions/language-server";
|
||||
|
||||
const settingsLanguageServerFormSchema = z.object({
|
||||
protocol: z.nativeEnum(LanguageServerProtocol),
|
||||
hostname: z.string(),
|
||||
port: z
|
||||
.number()
|
||||
.nullable()
|
||||
.transform((val) => (val === undefined ? null : val)),
|
||||
path: z
|
||||
.string()
|
||||
.nullable()
|
||||
.transform((val) => (val === "" || val === undefined ? null : val)),
|
||||
});
|
||||
|
||||
export type SettingsLanguageServerFormValues = z.infer<typeof settingsLanguageServerFormSchema>;
|
||||
|
||||
interface SettingsLanguageServerFormProps {
|
||||
defaultValues: Partial<SettingsLanguageServerFormValues>;
|
||||
language: EditorLanguage;
|
||||
}
|
||||
|
||||
export function SettingsLanguageServerForm({
|
||||
defaultValues,
|
||||
language,
|
||||
}: SettingsLanguageServerFormProps) {
|
||||
const form = useForm<SettingsLanguageServerFormValues>({
|
||||
resolver: zodResolver(settingsLanguageServerFormSchema),
|
||||
defaultValues,
|
||||
mode: "onChange",
|
||||
});
|
||||
|
||||
const onSubmit = async (data: SettingsLanguageServerFormValues) => {
|
||||
await handleLanguageServerConfigSubmit(language, data);
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="px-7">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="protocol"
|
||||
render={({ field }) => (
|
||||
<FormItem className="pt-0 pb-4 grid grid-cols-1 md:grid-cols-2 gap-4 items-start">
|
||||
<div className="space-y-2">
|
||||
<FormLabel>Protocol</FormLabel>
|
||||
<FormDescription>
|
||||
This is the protocol of the language server.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger className="w-full">
|
||||
<SelectValue placeholder="Select a protocol" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="wss">wss</SelectItem>
|
||||
<SelectItem value="ws">ws</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Separator />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="hostname"
|
||||
render={({ field }) => (
|
||||
<FormItem className="py-4 grid grid-cols-1 md:grid-cols-2 gap-4 items-start">
|
||||
<div className="space-y-2">
|
||||
<FormLabel>Hostname</FormLabel>
|
||||
<FormDescription>
|
||||
This is the hostname of the language server.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="w-full"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Separator />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="port"
|
||||
render={({ field }) => (
|
||||
<FormItem className="py-4 grid grid-cols-1 md:grid-cols-2 gap-4 items-start">
|
||||
<div className="space-y-2">
|
||||
<FormLabel>Port</FormLabel>
|
||||
<FormDescription>
|
||||
This is the port of the language server.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
type="number"
|
||||
value={field.value ?? ""}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value;
|
||||
field.onChange(value === "" ? null : Number(value));
|
||||
}}
|
||||
className="w-full"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Separator />
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="path"
|
||||
render={({ field }) => (
|
||||
<FormItem className="pt-4 pb-3 grid grid-cols-1 md:grid-cols-2 gap-4 items-start">
|
||||
<div className="space-y-2">
|
||||
<FormLabel>Path</FormLabel>
|
||||
<FormDescription>
|
||||
This is the path of the language server.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
value={field.value ?? ""}
|
||||
className="w-full"
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</div>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<div className="flex justify-end">
|
||||
<Button type="submit" className="w-full md:w-auto">
|
||||
Update
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
import { Suspense } from "react";
|
||||
import { Loading } from "@/components/loading";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
interface SettingsLanguageServerLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function SettingsLanguageServerLayout({
|
||||
children,
|
||||
}: SettingsLanguageServerLayoutProps) {
|
||||
return (
|
||||
<div className="container mx-auto max-w-[1024px] space-y-6">
|
||||
<div>
|
||||
<h3 className="text-lg font-medium">Language Server Settings</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Configure the language server connection settings.
|
||||
</p>
|
||||
</div>
|
||||
<Separator />
|
||||
<Suspense fallback={<Loading />}>
|
||||
{children}
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import { EditorLanguage } from "@/generated/client";
|
||||
import { getLanguageServerConfig } from "@/actions/language-server";
|
||||
import { LanguageServerAccordion } from "@/app/(app)/dashboard/@admin/settings/language-server/accordion";
|
||||
|
||||
export default async function SettingsLanguageServerPage() {
|
||||
const languages = Object.values(EditorLanguage);
|
||||
|
||||
const configPromises = languages.map(async (language) => ({
|
||||
language,
|
||||
config: await getLanguageServerConfig(language),
|
||||
}));
|
||||
|
||||
const configs = await Promise.all(configPromises);
|
||||
|
||||
return <LanguageServerAccordion configs={configs} />;
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
import { auth } from "@/lib/auth";
|
||||
import { User } from "@/generated/client";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
|
||||
interface DashboardLayoutProps {
|
||||
admin: React.ReactNode;
|
||||
}
|
||||
|
||||
export default async function DashboardLayout({
|
||||
admin,
|
||||
}: DashboardLayoutProps) {
|
||||
const session = await auth();
|
||||
if (!session?.user) {
|
||||
redirect("/sign-in");
|
||||
}
|
||||
|
||||
const user = session.user as User;
|
||||
|
||||
return user.role === "ADMIN" ? admin : notFound();
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import { Suspense } from "react";
|
||||
import { Loading } from "@/components/loading";
|
||||
|
||||
interface BotLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function BotLayout({ children }: BotLayoutProps) {
|
||||
return (
|
||||
<div className="flex flex-col h-full border border-t-0 border-muted rounded-b-3xl bg-background">
|
||||
<Suspense fallback={<Loading />}>
|
||||
{children}
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,127 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useCallback } from "react";
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useProblem } from "@/hooks/use-problem";
|
||||
import MdxPreview from "@/components/mdx-preview";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { BotIcon, SendHorizonal } from "lucide-react";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { ChatMessageList } from "@/components/ui/chat/chat-message-list";
|
||||
import { ChatBubble, ChatBubbleMessage } from "@/components/ui/chat/chat-bubble";
|
||||
|
||||
export default function Bot() {
|
||||
const t = useTranslations("Bot");
|
||||
const { problemId, problem, currentLang, currentValue } = useProblem();
|
||||
|
||||
const { messages, input, handleInputChange, setMessages, handleSubmit } = useChat({
|
||||
initialMessages: [
|
||||
{
|
||||
id: problemId,
|
||||
role: "system",
|
||||
content: `Problem description:\n${problem.description}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const handleFormSubmit = useCallback(
|
||||
(e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!input.trim()) {
|
||||
toast.error("Input cannot be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
const currentCodeMessage = {
|
||||
id: problemId,
|
||||
role: "system" as const,
|
||||
content: `Current code:\n\`\`\`${currentLang}\n${currentValue}\n\`\`\``,
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, currentCodeMessage]);
|
||||
handleSubmit();
|
||||
},
|
||||
[currentLang, currentValue, handleSubmit, input, problemId, setMessages]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex-1 relative">
|
||||
{!messages.some(
|
||||
(message) => message.role === "user" || message.role === "assistant"
|
||||
) && (
|
||||
<div className="h-full flex flex-col items-center justify-center gap-2 text-muted-foreground">
|
||||
<BotIcon />
|
||||
<span>{t("title")}</span>
|
||||
<span className="font-thin text-xs">{t("description")}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="absolute h-full w-full">
|
||||
<ScrollArea className="h-full [&>[data-radix-scroll-area-viewport]>div:min-w-0 [&>[data-radix-scroll-area-viewport]>div]:!block">
|
||||
<ChatMessageList>
|
||||
{messages
|
||||
.filter(
|
||||
(message) => message.role === "user" || message.role === "assistant"
|
||||
)
|
||||
.map((message) => (
|
||||
<ChatBubble key={message.id} layout="ai" className="border-b pb-4">
|
||||
<ChatBubbleMessage layout="ai">
|
||||
<MdxPreview source={message.content} />
|
||||
</ChatBubbleMessage>
|
||||
</ChatBubble>
|
||||
))}
|
||||
</ChatMessageList>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer className="h-36 flex flex-none">
|
||||
<form onSubmit={handleFormSubmit} className="w-full p-4 pt-0 relative">
|
||||
<Textarea
|
||||
value={input}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={(e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
if (input.trim()) {
|
||||
handleFormSubmit(e);
|
||||
} else {
|
||||
toast.error("Input cannot be empty");
|
||||
}
|
||||
}
|
||||
}}
|
||||
className="h-full bg-muted border-transparent shadow-none rounded-lg"
|
||||
placeholder={t("placeholder")}
|
||||
/>
|
||||
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="submit"
|
||||
variant="ghost"
|
||||
className="absolute bottom-6 right-6 h-6 w-auto px-2"
|
||||
aria-label="Send Message"
|
||||
>
|
||||
<SendHorizonal className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="px-2 py-1 text-xs">Ctrl + Enter</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</form>
|
||||
</footer>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
import { Suspense } from "react";
|
||||
import { Loading } from "@/components/loading";
|
||||
import { WorkspaceEditorHeader } from "@/components/features/playground/workspace/editor/components/header";
|
||||
import { WorkspaceEditorFooter } from "@/components/features/playground/workspace/editor/components/footer";
|
||||
|
||||
interface CodeLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function CodeLayout({ children }: CodeLayoutProps) {
|
||||
return (
|
||||
<div className="flex flex-col h-full border border-t-0 border-muted rounded-b-3xl bg-background">
|
||||
<WorkspaceEditorHeader />
|
||||
<Suspense fallback={<Loading />}>
|
||||
{children}
|
||||
</Suspense>
|
||||
<WorkspaceEditorFooter />
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
import { ProblemEditor } from "@/components/problem-editor";
|
||||
|
||||
export default function CodePage() {
|
||||
return (
|
||||
<div className="relative flex-1">
|
||||
<div className="absolute w-full h-full">
|
||||
<ProblemEditor />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import { Suspense } from "react";
|
||||
import { getUserLocale } from "@/i18n/locale";
|
||||
import { Loading } from "@/components/loading";
|
||||
import DetailsPage from "@/app/(app)/problems/[id]/@Details/page";
|
||||
|
||||
export default async function DetailsLayout() {
|
||||
const locale = await getUserLocale();
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full border border-t-0 border-muted rounded-b-3xl bg-background">
|
||||
<Suspense fallback={<Loading />}>
|
||||
<DetailsPage locale={locale} />;
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,211 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Locale } from "@/config/i18n";
|
||||
import { getLocale } from "@/lib/i18n";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { ArrowLeftIcon } from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useProblem } from "@/hooks/use-problem";
|
||||
import MdxPreview from "@/components/mdx-preview";
|
||||
import { useDockviewStore } from "@/stores/dockview";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { getStatusColorClass, statusMap } from "@/lib/status";
|
||||
import type { TestcaseResultWithTestcase } from "@/types/prisma";
|
||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||
import { formatDistanceToNow, isBefore, subDays, format } from "date-fns";
|
||||
|
||||
interface DetailsPageProps {
|
||||
locale: Locale;
|
||||
}
|
||||
|
||||
export default function DetailsPage({ locale }: DetailsPageProps) {
|
||||
const localeInstance = getLocale(locale);
|
||||
const t = useTranslations("DetailsPage");
|
||||
const s = useTranslations("StatusMessage");
|
||||
const { api, submission } = useDockviewStore();
|
||||
const { editorLanguageConfigs, problemId } = useProblem();
|
||||
const [lastFailedTestcase, setLastFailedTestcase] =
|
||||
useState<TestcaseResultWithTestcase | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!api || !problemId || !submission?.id) return;
|
||||
if (problemId !== submission.problemId) {
|
||||
const detailsPanel = api.getPanel("Details");
|
||||
if (detailsPanel) {
|
||||
api.removePanel(detailsPanel);
|
||||
}
|
||||
}
|
||||
}, [api, problemId, submission]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!submission?.id || !submission.testcaseResults) return;
|
||||
const failedTestcases = submission.testcaseResults.filter(
|
||||
(result) =>
|
||||
(submission.status === "WA" && !result.isCorrect) ||
|
||||
(submission.status === "TLE" && !result.isCorrect) ||
|
||||
(submission.status === "MLE" && !result.isCorrect) ||
|
||||
(submission.status === "RE" && !result.isCorrect)
|
||||
);
|
||||
setLastFailedTestcase(failedTestcases[0]);
|
||||
}, [submission]);
|
||||
|
||||
if (!api || !problemId || !submission?.id) return null;
|
||||
|
||||
const createdAt = new Date(submission.createdAt);
|
||||
const submittedDisplay = isBefore(createdAt, subDays(new Date(), 1))
|
||||
? format(createdAt, "yyyy-MM-dd")
|
||||
: formatDistanceToNow(createdAt, { addSuffix: true, locale: localeInstance });
|
||||
|
||||
const source = `\`\`\`${submission?.language}\n${submission?.code}\n\`\`\``;
|
||||
|
||||
const handleClick = () => {
|
||||
if (!api) return;
|
||||
const submissionsPanel = api.getPanel("Submissions");
|
||||
submissionsPanel?.api.setActive();
|
||||
const detailsPanel = api.getPanel("Details");
|
||||
if (detailsPanel) {
|
||||
api.removePanel(detailsPanel);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="h-8 flex flex-none items-center px-2 py-1 border-b">
|
||||
<Button
|
||||
onClick={handleClick}
|
||||
variant="ghost"
|
||||
className="h-8 w-auto p-2 hover:bg-transparent text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<ArrowLeftIcon size={16} aria-hidden="true" />
|
||||
<span>{t("BackButton")}</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="relative flex-1">
|
||||
<div className="absolute h-full w-full">
|
||||
<ScrollArea className="h-full">
|
||||
<div className="flex flex-col mx-auto max-w-[700px] gap-4 px-4 py-3">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex flex-1 flex-col items-start gap-1 overflow-hidden">
|
||||
<h3
|
||||
className={cn(
|
||||
"flex items-center text-xl font-semibold",
|
||||
getStatusColorClass(submission.status)
|
||||
)}
|
||||
>
|
||||
<span>{s(`${statusMap.get(submission.status)?.message}`)}</span>
|
||||
</h3>
|
||||
<div className="flex max-w-full flex-1 items-center gap-1 overflow-hidden text-xs">
|
||||
<span className="whitespace-nowrap mr-1">{t("Time")}</span>
|
||||
<span className="max-w-full truncate">
|
||||
{submittedDisplay}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-6">
|
||||
{lastFailedTestcase && (
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Accordion
|
||||
type="single"
|
||||
collapsible
|
||||
className="w-full -space-y-px"
|
||||
>
|
||||
<AccordionItem
|
||||
value="input"
|
||||
className="bg-background has-focus-visible:border-ring has-focus-visible:ring-ring/50 relative border px-4 py-1 outline-none first:rounded-t-md last:rounded-b-md last:border-b has-focus-visible:z-10 has-focus-visible:ring-[3px]"
|
||||
>
|
||||
<AccordionTrigger className="py-2 text-[15px] leading-6 hover:no-underline focus-visible:ring-0">
|
||||
<h4 className="text-sm font-medium">{t("Input")}</h4>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-muted-foreground pb-2">
|
||||
<div className="space-y-4">
|
||||
{lastFailedTestcase.testcase.data.map((field) => (
|
||||
<div key={field.id} className="space-y-2">
|
||||
<label className="text-sm font-medium">
|
||||
{`${field.label} =`}
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={field.value}
|
||||
readOnly
|
||||
className="bg-muted border-transparent shadow-none rounded-lg h-10"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium">{t("ExpectedOutput")}</h4>
|
||||
<Input
|
||||
type="text"
|
||||
value={lastFailedTestcase.testcase.expectedOutput}
|
||||
readOnly
|
||||
className="bg-muted border-transparent shadow-none rounded-lg h-10 font-mono"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{submission.status === "WA" && (
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-sm font-medium">{t("ActualOutput")}</h4>
|
||||
<Input
|
||||
type="text"
|
||||
value={lastFailedTestcase.output}
|
||||
readOnly
|
||||
className="bg-muted border-transparent shadow-none rounded-lg h-10 font-mono"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(submission.status === "CE" ||
|
||||
submission.status === "SE") && (
|
||||
<MdxPreview
|
||||
source={`\`\`\`shell\n${submission.message}\n\`\`\``}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="flex items-center pb-2">
|
||||
<div className="flex items-center gap-2 text-sm font-medium">
|
||||
<span>{t("Code")}</span>
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="h-4 bg-muted-foreground"
|
||||
/>
|
||||
<span>
|
||||
{
|
||||
editorLanguageConfigs.find(
|
||||
(config) =>
|
||||
config.language === submission.language
|
||||
)?.label
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<MdxPreview source={source} />
|
||||
</div>
|
||||
</div>
|
||||
<ScrollBar orientation="horizontal" />
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import { Suspense } from "react";
|
||||
import { Loading } from "@/components/loading";
|
||||
|
||||
interface SubmissionsLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function SubmissionsLayout({ children }: SubmissionsLayoutProps) {
|
||||
return (
|
||||
<div className="flex flex-col h-full px-3 border border-t-0 border-muted rounded-b-3xl bg-background">
|
||||
<Suspense fallback={<Loading />}>
|
||||
{children}
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getUserLocale } from "@/i18n/locale";
|
||||
import SubmissionsTable from "@/components/submissions-table";
|
||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||
import SubmissionLoginButton from "@/components/submission-login-button";
|
||||
|
||||
interface SubmissionsPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default async function SubmissionsPage({ params }: SubmissionsPageProps) {
|
||||
const { id } = await params;
|
||||
const session = await auth();
|
||||
|
||||
if (!id) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return (
|
||||
<SubmissionLoginButton />
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
const problem = await prisma.problem.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
submissions: {
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
},
|
||||
include: {
|
||||
testcaseResults: {
|
||||
include: {
|
||||
testcase: {
|
||||
include: {
|
||||
data: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!problem) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const locale = await getUserLocale();
|
||||
|
||||
return (
|
||||
<>
|
||||
<ScrollArea className="h-full">
|
||||
<SubmissionsTable locale={locale} submissions={problem.submissions} />
|
||||
<ScrollBar orientation="horizontal" />
|
||||
</ScrollArea>
|
||||
</>
|
||||
);
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
import { Suspense } from "react";
|
||||
import { Loading } from "@/components/loading";
|
||||
|
||||
interface TestcaseLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function TestcaseLayout({ children }: TestcaseLayoutProps) {
|
||||
return (
|
||||
<div className="relative h-full border border-t-0 border-muted rounded-b-3xl bg-background">
|
||||
<Suspense fallback={<Loading />}>
|
||||
{children}
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { notFound } from "next/navigation";
|
||||
import TestcaseCard from "@/components/testcase-card";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
|
||||
interface TestcasePageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
}
|
||||
|
||||
export default async function TestcasePage({ params }: TestcasePageProps) {
|
||||
const { id } = await params;
|
||||
|
||||
if (!id) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const problem = await prisma.problem.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
testcases: {
|
||||
include: {
|
||||
data: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!problem) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="absolute h-full w-full">
|
||||
<ScrollArea className="h-full">
|
||||
<TestcaseCard testcases={problem.testcases} />
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,148 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
BotIcon,
|
||||
CircleCheckBigIcon,
|
||||
FileTextIcon,
|
||||
FlaskConicalIcon,
|
||||
SquareCheckIcon,
|
||||
SquarePenIcon,
|
||||
} from "lucide-react";
|
||||
import { Locale } from "@/config/i18n";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Dockview from "@/components/dockview";
|
||||
import { useDockviewStore } from "@/stores/dockview";
|
||||
|
||||
interface ProblemPageProps {
|
||||
locale: Locale;
|
||||
Description: React.ReactNode;
|
||||
Solutions: React.ReactNode;
|
||||
Submissions: React.ReactNode;
|
||||
Details: React.ReactNode;
|
||||
Code: React.ReactNode;
|
||||
Testcase: React.ReactNode;
|
||||
Bot: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function ProblemPage({
|
||||
locale,
|
||||
Description,
|
||||
Solutions,
|
||||
Submissions,
|
||||
Details,
|
||||
Code,
|
||||
Testcase,
|
||||
Bot,
|
||||
}: ProblemPageProps) {
|
||||
const [key, setKey] = useState(0);
|
||||
const { setApi } = useDockviewStore();
|
||||
const t = useTranslations("ProblemPage");
|
||||
|
||||
useEffect(() => {
|
||||
setKey((prevKey) => prevKey + 1);
|
||||
}, [locale]);
|
||||
|
||||
return (
|
||||
<Dockview
|
||||
key={key}
|
||||
storageKey="dockview:problem"
|
||||
onApiReady={setApi}
|
||||
options={[
|
||||
{
|
||||
id: "Description",
|
||||
component: "Description",
|
||||
tabComponent: "Description",
|
||||
params: {
|
||||
icon: FileTextIcon,
|
||||
content: Description,
|
||||
title: t("Description"),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "Solutions",
|
||||
component: "Solutions",
|
||||
tabComponent: "Solutions",
|
||||
params: {
|
||||
icon: FlaskConicalIcon,
|
||||
content: Solutions,
|
||||
title: t("Solutions"),
|
||||
},
|
||||
position: {
|
||||
referencePanel: "Description",
|
||||
direction: "within",
|
||||
},
|
||||
inactive: true,
|
||||
},
|
||||
{
|
||||
id: "Submissions",
|
||||
component: "Submissions",
|
||||
tabComponent: "Submissions",
|
||||
params: {
|
||||
icon: CircleCheckBigIcon,
|
||||
content: Submissions,
|
||||
title: t("Submissions"),
|
||||
},
|
||||
position: {
|
||||
referencePanel: "Solutions",
|
||||
direction: "within",
|
||||
},
|
||||
inactive: true,
|
||||
},
|
||||
{
|
||||
id: "Details",
|
||||
component: "Details",
|
||||
tabComponent: "Details",
|
||||
params: {
|
||||
icon: CircleCheckBigIcon,
|
||||
content: Details,
|
||||
title: t("Details"),
|
||||
autoAdd: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "Code",
|
||||
component: "Code",
|
||||
tabComponent: "Code",
|
||||
params: {
|
||||
icon: SquarePenIcon,
|
||||
content: Code,
|
||||
title: t("Code"),
|
||||
},
|
||||
position: {
|
||||
referencePanel: "Submissions",
|
||||
direction: "right",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "Testcase",
|
||||
component: "Testcase",
|
||||
tabComponent: "Testcase",
|
||||
params: {
|
||||
icon: SquareCheckIcon,
|
||||
content: Testcase,
|
||||
title: t("Testcase"),
|
||||
},
|
||||
position: {
|
||||
referencePanel: "Code",
|
||||
direction: "below",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "Bot",
|
||||
component: "Bot",
|
||||
tabComponent: "Bot",
|
||||
params: {
|
||||
icon: BotIcon,
|
||||
content: Bot,
|
||||
title: t("Bot"),
|
||||
autoAdd: false,
|
||||
},
|
||||
position: {
|
||||
direction: "right",
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
@ -19,7 +19,9 @@ export default async function ProblemLayout({
|
||||
return (
|
||||
<div className="flex flex-col h-screen">
|
||||
<ProblemHeader />
|
||||
{children}
|
||||
<div className="flex w-full flex-grow overflow-y-hidden p-2.5 pt-0">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
39
src/app/(app)/problems/[problemId]/page.tsx
Normal file
39
src/app/(app)/problems/[problemId]/page.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { TestcasePanel } from "@/features/problems/testcase/panel";
|
||||
import { BotPanel } from "@/features/problems/bot/components/panel";
|
||||
import { CodePanel } from "@/features/problems/code/components/panel";
|
||||
import { DetailPanel } from "@/features/problems/detail/components/panel";
|
||||
import { SolutionPanel } from "@/features/problems/solution/components/panel";
|
||||
import { SubmissionPanel } from "@/features/problems/submission/components/panel";
|
||||
import { DescriptionPanel } from "@/features/problems/description/components/panel";
|
||||
import { ProblemFlexLayout } from "@/features/problems/components/problem-flexlayout";
|
||||
|
||||
interface ProblemPageProps {
|
||||
params: Promise<{ problemId: string }>;
|
||||
searchParams: Promise<{
|
||||
submissionId: string | undefined;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function ProblemPage({
|
||||
params,
|
||||
searchParams,
|
||||
}: ProblemPageProps) {
|
||||
const { problemId } = await params;
|
||||
const { submissionId } = await searchParams;
|
||||
|
||||
const components: Record<string, React.ReactNode> = {
|
||||
description: <DescriptionPanel problemId={problemId} />,
|
||||
solution: <SolutionPanel problemId={problemId} />,
|
||||
submission: <SubmissionPanel problemId={problemId} />,
|
||||
detail: <DetailPanel submissionId={submissionId} />,
|
||||
code: <CodePanel problemId={problemId} />,
|
||||
testcase: <TestcasePanel problemId={problemId} />,
|
||||
bot: <BotPanel problemId={problemId} />,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative flex h-full w-full">
|
||||
<ProblemFlexLayout components={components} />
|
||||
</div>
|
||||
);
|
||||
}
|
27
src/app/(auth)/layout.tsx
Normal file
27
src/app/(auth)/layout.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
interface AuthLayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export default function AuthLayout({ children }: AuthLayoutProps) {
|
||||
const t = useTranslations("Video");
|
||||
|
||||
return (
|
||||
<div className="grid min-h-svh lg:grid-cols-2">
|
||||
{children}
|
||||
<div className="relative hidden bg-muted lg:block">
|
||||
<video
|
||||
autoPlay
|
||||
loop
|
||||
muted
|
||||
playsInline
|
||||
className="absolute inset-0 h-full w-full object-cover"
|
||||
>
|
||||
<source src="/sign-in.mp4" type="video/mp4" />
|
||||
{t("unsupportedBrowser")}
|
||||
</video>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,10 +1,10 @@
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { CodeIcon } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { providerMap, signIn } from "@/lib/auth";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { FaGithub, FaGoogle } from "react-icons/fa";
|
||||
import { CredentialsSignInForm } from "@/components/credentials-sign-in-form";
|
||||
|
||||
interface ProviderIconProps {
|
||||
providerId: string;
|
||||
@ -32,67 +32,71 @@ export default async function SignInPage({ searchParams }: SignInPageProps) {
|
||||
const t = await getTranslations("SignInForm");
|
||||
|
||||
return (
|
||||
<div className="grid min-h-svh lg:grid-cols-2">
|
||||
<div className="flex flex-col gap-4 p-6 md:p-10">
|
||||
<div className="flex justify-center gap-2 md:justify-start">
|
||||
<Link
|
||||
href={callbackUrl ?? "/"}
|
||||
className="flex items-center gap-2 font-medium"
|
||||
>
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-md bg-primary text-primary-foreground">
|
||||
<CodeIcon className="size-4" />
|
||||
<div className="flex flex-col gap-4 p-6 md:p-10">
|
||||
<div className="flex justify-center gap-2 md:justify-start">
|
||||
<Link
|
||||
href={callbackUrl ?? "/"}
|
||||
className="flex items-center gap-2 font-medium"
|
||||
>
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-md bg-primary text-primary-foreground">
|
||||
<CodeIcon className="size-4" />
|
||||
</div>
|
||||
Judge4c
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<div className="w-full max-w-sm">
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col items-center gap-2 text-center">
|
||||
<h1 className="text-2xl font-bold">{t("title")}</h1>
|
||||
<p className="text-balance text-sm text-muted-foreground">
|
||||
{t("description")}
|
||||
</p>
|
||||
</div>
|
||||
Judge4c
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<div className="w-full max-w-sm">
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col items-center gap-2 text-center">
|
||||
<h1 className="text-2xl font-bold">{t("title")}</h1>
|
||||
<p className="text-balance text-sm text-muted-foreground line-through">
|
||||
{t("description")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t after:border-border">
|
||||
<span className="relative z-10 bg-background px-2 text-muted-foreground">
|
||||
{t("or")}
|
||||
</span>
|
||||
</div>
|
||||
{Object.values(providerMap).map((provider) => {
|
||||
return (
|
||||
<form
|
||||
key={provider.id}
|
||||
action={async () => {
|
||||
"use server";
|
||||
await signIn(provider.id, {
|
||||
redirectTo: callbackUrl ?? "",
|
||||
});
|
||||
}}
|
||||
<CredentialsSignInForm callbackUrl={callbackUrl} />
|
||||
<div className="relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t after:border-border">
|
||||
<span className="relative z-10 bg-background px-2 text-muted-foreground">
|
||||
{t("or")}
|
||||
</span>
|
||||
</div>
|
||||
{Object.values(providerMap).map((provider) => {
|
||||
return (
|
||||
<form
|
||||
key={provider.id}
|
||||
action={async () => {
|
||||
"use server";
|
||||
await signIn(provider.id, {
|
||||
redirectTo: callbackUrl,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full flex items-center justify-center gap-4"
|
||||
type="submit"
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full flex items-center justify-center gap-4"
|
||||
type="submit"
|
||||
>
|
||||
<ProviderIcon providerId={provider.id} />
|
||||
{t("oauth", { provider: provider.name })}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
})}
|
||||
<ProviderIcon providerId={provider.id} />
|
||||
{t("oauth", { provider: provider.name })}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
})}
|
||||
<div className="text-center text-sm">
|
||||
{t("noAccount")}{" "}
|
||||
<Link
|
||||
href={`/sign-up${
|
||||
callbackUrl
|
||||
? `?callbackUrl=${encodeURIComponent(callbackUrl)}`
|
||||
: ""
|
||||
}`}
|
||||
className="underline underline-offset-4"
|
||||
>
|
||||
{t("signUp")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative hidden bg-muted lg:block">
|
||||
<Image
|
||||
src="/placeholder.svg"
|
||||
alt="Image"
|
||||
fill
|
||||
className="absolute inset-0 h-full w-full object-cover dark:brightness-[0.2] dark:grayscale"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
102
src/app/(auth)/sign-up/page.tsx
Normal file
102
src/app/(auth)/sign-up/page.tsx
Normal file
@ -0,0 +1,102 @@
|
||||
import Link from "next/link";
|
||||
import { CodeIcon } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { providerMap, signIn } from "@/lib/auth";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { FaGithub, FaGoogle } from "react-icons/fa";
|
||||
import { CredentialsSignUpForm } from "@/components/credentials-sign-up-form";
|
||||
|
||||
interface ProviderIconProps {
|
||||
providerId: string;
|
||||
}
|
||||
|
||||
const ProviderIcon = ({ providerId }: ProviderIconProps) => {
|
||||
switch (providerId) {
|
||||
case "github":
|
||||
return <FaGithub />;
|
||||
case "google":
|
||||
return <FaGoogle />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
interface SignUpPageProps {
|
||||
searchParams: Promise<{
|
||||
callbackUrl: string | undefined;
|
||||
}>;
|
||||
}
|
||||
|
||||
export default async function SignInPage({ searchParams }: SignUpPageProps) {
|
||||
const { callbackUrl } = await searchParams;
|
||||
const t = await getTranslations("SignUpForm");
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 p-6 md:p-10">
|
||||
<div className="flex justify-center gap-2 md:justify-start">
|
||||
<Link
|
||||
href={callbackUrl ?? "/"}
|
||||
className="flex items-center gap-2 font-medium"
|
||||
>
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-md bg-primary text-primary-foreground">
|
||||
<CodeIcon className="size-4" />
|
||||
</div>
|
||||
Judge4c
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex flex-1 items-center justify-center">
|
||||
<div className="w-full max-w-sm">
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-col items-center gap-2 text-center">
|
||||
<h1 className="text-2xl font-bold">{t("title")}</h1>
|
||||
<p className="text-balance text-sm text-muted-foreground">
|
||||
{t("description")}
|
||||
</p>
|
||||
</div>
|
||||
<CredentialsSignUpForm callbackUrl={callbackUrl} />
|
||||
<div className="relative text-center text-sm after:absolute after:inset-0 after:top-1/2 after:z-0 after:flex after:items-center after:border-t after:border-border">
|
||||
<span className="relative z-10 bg-background px-2 text-muted-foreground">
|
||||
{t("or")}
|
||||
</span>
|
||||
</div>
|
||||
{Object.values(providerMap).map((provider) => {
|
||||
return (
|
||||
<form
|
||||
key={provider.id}
|
||||
action={async () => {
|
||||
"use server";
|
||||
await signIn(provider.id, {
|
||||
redirectTo: callbackUrl,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full flex items-center justify-center gap-4"
|
||||
type="submit"
|
||||
>
|
||||
<ProviderIcon providerId={provider.id} />
|
||||
{t("oauth", { provider: provider.name })}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
})}
|
||||
<div className="text-center text-sm">
|
||||
{t("haveAccount")}{" "}
|
||||
<Link
|
||||
href={`/sign-in${
|
||||
callbackUrl
|
||||
? `?callbackUrl=${encodeURIComponent(callbackUrl)}`
|
||||
: ""
|
||||
}`}
|
||||
className="underline underline-offset-4"
|
||||
>
|
||||
{t("signIn")}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
6
src/app/actions/analyze.ts
Normal file
6
src/app/actions/analyze.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import { Complexity } from "@/types/complexity";
|
||||
|
||||
export const analyzeComplexity = async (content: string) => {
|
||||
console.log("🚀 ~ analyzeComplexity ~ content:", content);
|
||||
return { time: Complexity.Enum["O(N)"], space: Complexity.Enum["O(1)"] };
|
||||
};
|
93
src/app/actions/auth.ts
Normal file
93
src/app/actions/auth.ts
Normal file
@ -0,0 +1,93 @@
|
||||
"use server";
|
||||
|
||||
import bcrypt from "bcryptjs";
|
||||
import prisma from "@/lib/prisma";
|
||||
import { signIn } from "@/lib/auth";
|
||||
import { authSchema } from "@/lib/zod";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
import { CredentialsSignInFormValues } from "@/components/credentials-sign-in-form";
|
||||
import { CredentialsSignUpFormValues } from "@/components/credentials-sign-up-form";
|
||||
|
||||
const saltRounds = 10;
|
||||
|
||||
export const signInWithCredentials = async (
|
||||
formData: CredentialsSignInFormValues
|
||||
) => {
|
||||
const t = await getTranslations("signInWithCredentials");
|
||||
|
||||
try {
|
||||
// Parse credentials using authSchema for validation
|
||||
const { email, password } = await authSchema.parseAsync(formData);
|
||||
|
||||
// Find user by email
|
||||
const user = await prisma.user.findUnique({ where: { email } });
|
||||
|
||||
// Check if the user exists
|
||||
if (!user) {
|
||||
throw new Error(t("userNotFound"));
|
||||
}
|
||||
|
||||
// Check if the user has a password
|
||||
if (!user.password) {
|
||||
throw new Error(t("invalidCredentials"));
|
||||
}
|
||||
|
||||
// Check if the password matches
|
||||
const passwordMatch = await bcrypt.compare(password, user.password);
|
||||
if (!passwordMatch) {
|
||||
throw new Error(t("incorrectPassword"));
|
||||
}
|
||||
|
||||
await signIn("credentials", {
|
||||
...formData,
|
||||
redirect: false,
|
||||
});
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return {
|
||||
error: error instanceof Error ? error.message : t("signInFailedFallback"),
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const signUpWithCredentials = async (
|
||||
formData: CredentialsSignUpFormValues
|
||||
) => {
|
||||
const t = await getTranslations("signUpWithCredentials");
|
||||
|
||||
try {
|
||||
const validatedData = await authSchema.parseAsync(formData);
|
||||
|
||||
// Check if user already exists
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { email: validatedData.email },
|
||||
});
|
||||
if (existingUser) {
|
||||
throw new Error(t("userAlreadyExists"));
|
||||
}
|
||||
|
||||
// Hash password and create user
|
||||
const pwHash = await bcrypt.hash(validatedData.password, saltRounds);
|
||||
const user = await prisma.user.create({
|
||||
data: { email: validatedData.email, password: pwHash },
|
||||
});
|
||||
|
||||
// Assign admin role if first user
|
||||
const userCount = await prisma.user.count();
|
||||
if (userCount === 1) {
|
||||
await prisma.user.update({
|
||||
where: { id: user.id },
|
||||
data: { role: "ADMIN" },
|
||||
});
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
return {
|
||||
error:
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: t("registrationFailedFallback"),
|
||||
};
|
||||
}
|
||||
};
|
102
src/app/actions/compile.ts
Normal file
102
src/app/actions/compile.ts
Normal file
@ -0,0 +1,102 @@
|
||||
import "server-only";
|
||||
|
||||
import Docker from "dockerode";
|
||||
import prisma from "@/lib/prisma";
|
||||
import { createLimitedStream, docker } from "./docker";
|
||||
import { type DockerConfig, Language, Status } from "@/generated/client";
|
||||
|
||||
const getCompileCmdForLanguage = (language: Language) => {
|
||||
switch (language) {
|
||||
case Language.c:
|
||||
return ["gcc", "-O2", "main.c", "-o", "main"];
|
||||
case Language.cpp:
|
||||
return ["g++", "-O2", "main.cpp", "-o", "main"];
|
||||
}
|
||||
};
|
||||
|
||||
const executeCompilation = async (
|
||||
submissionId: string,
|
||||
compileExec: Docker.Exec,
|
||||
compileOutputLimit: number
|
||||
): Promise<Status> => {
|
||||
return new Promise<Status>((resolve, reject) => {
|
||||
compileExec.start({}, async (error, stream) => {
|
||||
if (error || !stream) {
|
||||
reject(Status.SE);
|
||||
return;
|
||||
}
|
||||
|
||||
const { stream: stdoutStream } = createLimitedStream(compileOutputLimit);
|
||||
const { stream: stderrStream, buffers: stderrBuffers } =
|
||||
createLimitedStream(compileOutputLimit);
|
||||
|
||||
docker.modem.demuxStream(stream, stdoutStream, stderrStream);
|
||||
|
||||
stream.on("end", async () => {
|
||||
const stderr = stderrBuffers.join("");
|
||||
const exitCode = (await compileExec.inspect()).ExitCode;
|
||||
|
||||
if (exitCode === 0) {
|
||||
resolve(Status.CS);
|
||||
} else {
|
||||
await prisma.submission.update({
|
||||
where: {
|
||||
id: submissionId,
|
||||
},
|
||||
data: {
|
||||
message: stderr,
|
||||
},
|
||||
});
|
||||
resolve(Status.CE);
|
||||
}
|
||||
});
|
||||
|
||||
stream.on("error", async () => {
|
||||
reject(Status.SE);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const compile = async (
|
||||
container: Docker.Container,
|
||||
language: Language,
|
||||
submissionId: string,
|
||||
config: DockerConfig
|
||||
): Promise<Status> => {
|
||||
const { compileOutputLimit } = config;
|
||||
|
||||
await prisma.submission.update({
|
||||
where: {
|
||||
id: submissionId,
|
||||
},
|
||||
data: {
|
||||
status: Status.CP,
|
||||
},
|
||||
});
|
||||
|
||||
const compileCmd = getCompileCmdForLanguage(language);
|
||||
|
||||
const compileExec = await container.exec({
|
||||
Cmd: compileCmd,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
});
|
||||
|
||||
const status = await executeCompilation(
|
||||
submissionId,
|
||||
compileExec,
|
||||
compileOutputLimit
|
||||
);
|
||||
|
||||
await prisma.submission.update({
|
||||
where: {
|
||||
id: submissionId,
|
||||
},
|
||||
data: {
|
||||
status,
|
||||
},
|
||||
});
|
||||
|
||||
return status;
|
||||
};
|
107
src/app/actions/docker.ts
Normal file
107
src/app/actions/docker.ts
Normal file
@ -0,0 +1,107 @@
|
||||
import "server-only";
|
||||
|
||||
import fs from "fs";
|
||||
import tar from "tar-stream";
|
||||
import Docker from "dockerode";
|
||||
import { Readable } from "stream";
|
||||
import { Writable } from "stream";
|
||||
import type { DockerConfig } from "@/generated/client";
|
||||
|
||||
const isRemote = process.env.DOCKER_HOST_MODE === "remote";
|
||||
|
||||
// Docker client initialization
|
||||
export const docker: Docker = isRemote
|
||||
? new Docker({
|
||||
protocol: process.env.DOCKER_REMOTE_PROTOCOL as
|
||||
| "https"
|
||||
| "http"
|
||||
| "ssh"
|
||||
| undefined,
|
||||
host: process.env.DOCKER_REMOTE_HOST,
|
||||
port: process.env.DOCKER_REMOTE_PORT,
|
||||
ca: fs.readFileSync(process.env.DOCKER_REMOTE_CA_PATH || "/certs/ca.pem"),
|
||||
cert: fs.readFileSync(
|
||||
process.env.DOCKER_REMOTE_CERT_PATH || "/certs/cert.pem"
|
||||
),
|
||||
key: fs.readFileSync(
|
||||
process.env.DOCKER_REMOTE_KEY_PATH || "/certs/key.pem"
|
||||
),
|
||||
})
|
||||
: new Docker({ socketPath: "/var/run/docker.sock" });
|
||||
|
||||
// Prepare Docker image environment
|
||||
export const prepareEnvironment = async (
|
||||
image: string,
|
||||
tag: string
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
const reference = `${image}:${tag}`;
|
||||
const filters = { reference: [reference] };
|
||||
const images = await docker.listImages({ filters });
|
||||
return images.length !== 0;
|
||||
} catch (error) {
|
||||
console.error("Error checking Docker images:", error);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
// Create Docker container with keep-alive
|
||||
export const createContainer = async (
|
||||
config: DockerConfig,
|
||||
memoryLimit: number
|
||||
): Promise<Docker.Container> => {
|
||||
const { image, tag, workingDir } = config;
|
||||
const container = await docker.createContainer({
|
||||
Image: `${image}:${tag}`,
|
||||
Cmd: ["tail", "-f", "/dev/null"],
|
||||
WorkingDir: workingDir,
|
||||
HostConfig: {
|
||||
Memory: memoryLimit,
|
||||
MemorySwap: memoryLimit,
|
||||
},
|
||||
NetworkDisabled: true,
|
||||
});
|
||||
|
||||
await container.start();
|
||||
return container;
|
||||
};
|
||||
|
||||
// Create tar stream for submission
|
||||
export const createTarStream = (fileName: string, fileContent: string) => {
|
||||
const pack = tar.pack();
|
||||
pack.entry({ name: fileName }, fileContent);
|
||||
pack.finalize();
|
||||
return Readable.from(pack);
|
||||
};
|
||||
|
||||
export const createLimitedStream = (maxSize: number) => {
|
||||
const buffers: string[] = [];
|
||||
let totalLength = 0;
|
||||
|
||||
const stream = new Writable({
|
||||
write(chunk, _encoding, callback) {
|
||||
const text = chunk.toString();
|
||||
const remaining = maxSize - totalLength;
|
||||
|
||||
if (remaining <= 0) {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
if (text.length > remaining) {
|
||||
buffers.push(text.slice(0, remaining));
|
||||
totalLength = maxSize;
|
||||
} else {
|
||||
buffers.push(text);
|
||||
totalLength += text.length;
|
||||
}
|
||||
|
||||
callback();
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
stream,
|
||||
buffers,
|
||||
};
|
||||
};
|
174
src/app/actions/judge.ts
Normal file
174
src/app/actions/judge.ts
Normal file
@ -0,0 +1,174 @@
|
||||
"use server";
|
||||
|
||||
import { run } from "./run";
|
||||
import Docker from "dockerode";
|
||||
import prisma from "@/lib/prisma";
|
||||
import { compile } from "./compile";
|
||||
import { auth, signIn } from "@/lib/auth";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { Language, Status } from "@/generated/client";
|
||||
import { createContainer, createTarStream, prepareEnvironment } from "./docker";
|
||||
|
||||
export const judge = async (
|
||||
problemId: string,
|
||||
language: Language,
|
||||
content: string
|
||||
): Promise<Status> => {
|
||||
const session = await auth();
|
||||
const userId = session?.user?.id;
|
||||
if (!userId) {
|
||||
await signIn();
|
||||
return Status.SE;
|
||||
}
|
||||
|
||||
let container: Docker.Container | null = null;
|
||||
|
||||
try {
|
||||
const problem = await prisma.problem.findUnique({
|
||||
where: {
|
||||
id: problemId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!problem) {
|
||||
await prisma.submission.create({
|
||||
data: {
|
||||
language,
|
||||
content,
|
||||
status: Status.SE,
|
||||
message: "Problem not found",
|
||||
userId,
|
||||
problemId,
|
||||
},
|
||||
});
|
||||
return Status.SE;
|
||||
}
|
||||
|
||||
const testcases = await prisma.testcase.findMany({
|
||||
where: {
|
||||
problemId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!testcases.length) {
|
||||
await prisma.submission.create({
|
||||
data: {
|
||||
language,
|
||||
content,
|
||||
status: Status.SE,
|
||||
message: "No testcases available for this problem",
|
||||
userId,
|
||||
problemId,
|
||||
},
|
||||
});
|
||||
return Status.SE;
|
||||
}
|
||||
|
||||
const dockerConfig = await prisma.dockerConfig.findUnique({
|
||||
where: {
|
||||
language,
|
||||
},
|
||||
});
|
||||
|
||||
if (!dockerConfig) {
|
||||
await prisma.submission.create({
|
||||
data: {
|
||||
language,
|
||||
content,
|
||||
status: Status.SE,
|
||||
message: `Docker configuration not found for language: ${language}`,
|
||||
userId,
|
||||
problemId,
|
||||
},
|
||||
});
|
||||
return Status.SE;
|
||||
}
|
||||
|
||||
const dockerPrepared = await prepareEnvironment(
|
||||
dockerConfig.image,
|
||||
dockerConfig.tag
|
||||
);
|
||||
|
||||
if (!dockerPrepared) {
|
||||
console.error(
|
||||
"Docker image not found:",
|
||||
dockerConfig.image,
|
||||
":",
|
||||
dockerConfig.tag
|
||||
);
|
||||
await prisma.submission.create({
|
||||
data: {
|
||||
language,
|
||||
content,
|
||||
status: Status.SE,
|
||||
message: `Docker image not found: ${dockerConfig.image}:${dockerConfig.tag}`,
|
||||
userId,
|
||||
problemId,
|
||||
},
|
||||
});
|
||||
return Status.SE;
|
||||
}
|
||||
|
||||
const submission = await prisma.submission.create({
|
||||
data: {
|
||||
language,
|
||||
content,
|
||||
status: Status.PD,
|
||||
userId,
|
||||
problemId,
|
||||
},
|
||||
});
|
||||
|
||||
// Upload code to the container
|
||||
const tarStream = createTarStream(
|
||||
getFileNameForLanguage(language),
|
||||
content
|
||||
);
|
||||
|
||||
container = await createContainer(dockerConfig, problem.memoryLimit);
|
||||
await container.putArchive(tarStream, { path: dockerConfig.workingDir });
|
||||
|
||||
// Compile the code
|
||||
const compileStatus = await compile(
|
||||
container,
|
||||
language,
|
||||
submission.id,
|
||||
dockerConfig
|
||||
);
|
||||
|
||||
if (compileStatus !== "CS") return compileStatus;
|
||||
|
||||
const runStatus = await run(
|
||||
container,
|
||||
language,
|
||||
submission.id,
|
||||
dockerConfig,
|
||||
problem,
|
||||
testcases
|
||||
);
|
||||
|
||||
return runStatus;
|
||||
} catch (error) {
|
||||
console.error("Error in judge:", error);
|
||||
return Status.SE;
|
||||
} finally {
|
||||
revalidatePath(`/problems/${problemId}`);
|
||||
if (container) {
|
||||
try {
|
||||
await container.kill();
|
||||
await container.remove();
|
||||
} catch (error) {
|
||||
console.error("Container cleanup failed:", error);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const getFileNameForLanguage = (language: Language) => {
|
||||
switch (language) {
|
||||
case Language.c:
|
||||
return "main.c";
|
||||
case Language.cpp:
|
||||
return "main.cpp";
|
||||
}
|
||||
};
|
253
src/app/actions/run.ts
Normal file
253
src/app/actions/run.ts
Normal file
@ -0,0 +1,253 @@
|
||||
import "server-only";
|
||||
|
||||
import {
|
||||
DockerConfig,
|
||||
Language,
|
||||
Problem,
|
||||
Status,
|
||||
Testcase,
|
||||
} from "@/generated/client";
|
||||
import Docker from "dockerode";
|
||||
import prisma from "@/lib/prisma";
|
||||
import { createLimitedStream, docker } from "./docker";
|
||||
|
||||
const getRunCmdForLanguage = (language: Language) => {
|
||||
switch (language) {
|
||||
case Language.c:
|
||||
return ["./main"];
|
||||
case Language.cpp:
|
||||
return ["./main"];
|
||||
}
|
||||
};
|
||||
|
||||
const startRun = (
|
||||
runExec: Docker.Exec,
|
||||
runOutputLimit: number,
|
||||
submissionId: string,
|
||||
testcaseId: string,
|
||||
joinedInputs: string,
|
||||
timeLimit: number,
|
||||
memoryLimit: number,
|
||||
expectedOutput: string
|
||||
): Promise<Status> => {
|
||||
return new Promise<Status>((resolve, reject) => {
|
||||
runExec.start({ hijack: true }, async (error, stream) => {
|
||||
if (error || !stream) {
|
||||
await prisma.testcaseResult.create({
|
||||
data: {
|
||||
isCorrect: false,
|
||||
submissionId,
|
||||
testcaseId,
|
||||
},
|
||||
});
|
||||
reject(Status.SE);
|
||||
return;
|
||||
}
|
||||
stream.write(joinedInputs);
|
||||
stream.end();
|
||||
|
||||
const { stream: stdoutStream, buffers: stdoutBuffers } =
|
||||
createLimitedStream(runOutputLimit);
|
||||
const { stream: stderrStream } = createLimitedStream(runOutputLimit);
|
||||
docker.modem.demuxStream(stream, stdoutStream, stderrStream);
|
||||
|
||||
const startTime = Date.now();
|
||||
const timeoutId = setTimeout(async () => {
|
||||
stream.destroy();
|
||||
await prisma.testcaseResult.create({
|
||||
data: {
|
||||
isCorrect: false,
|
||||
timeUsage: timeLimit,
|
||||
submissionId,
|
||||
testcaseId,
|
||||
},
|
||||
});
|
||||
resolve(Status.TLE);
|
||||
}, timeLimit);
|
||||
|
||||
stream.on("end", async () => {
|
||||
clearTimeout(timeoutId);
|
||||
const stdout = stdoutBuffers.join("");
|
||||
const exitCode = (await runExec.inspect()).ExitCode;
|
||||
const timeUsage = Date.now() - startTime;
|
||||
if (exitCode === 0) {
|
||||
const isCorrect = stdout.trim() === expectedOutput;
|
||||
await prisma.testcaseResult.create({
|
||||
data: {
|
||||
isCorrect,
|
||||
output: stdout,
|
||||
timeUsage,
|
||||
submissionId,
|
||||
testcaseId,
|
||||
},
|
||||
});
|
||||
if (isCorrect) {
|
||||
resolve(Status.RU);
|
||||
} else {
|
||||
resolve(Status.WA);
|
||||
}
|
||||
} else if (exitCode === 137) {
|
||||
await prisma.testcaseResult.create({
|
||||
data: {
|
||||
isCorrect: false,
|
||||
timeUsage,
|
||||
memoryUsage: memoryLimit,
|
||||
submissionId,
|
||||
testcaseId,
|
||||
},
|
||||
});
|
||||
resolve(Status.MLE);
|
||||
} else {
|
||||
await prisma.testcaseResult.create({
|
||||
data: {
|
||||
isCorrect: false,
|
||||
submissionId,
|
||||
testcaseId,
|
||||
},
|
||||
});
|
||||
resolve(Status.RE);
|
||||
}
|
||||
});
|
||||
|
||||
stream.on("error", async () => {
|
||||
clearTimeout(timeoutId);
|
||||
await prisma.testcaseResult.create({
|
||||
data: {
|
||||
isCorrect: false,
|
||||
submissionId,
|
||||
testcaseId,
|
||||
},
|
||||
});
|
||||
reject(Status.SE);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const executeRun = async (
|
||||
container: Docker.Container,
|
||||
runCmd: string[],
|
||||
runOutputLimit: number,
|
||||
submissionId: string,
|
||||
timeLimit: number,
|
||||
memoryLimit: number,
|
||||
testcases: Testcase[]
|
||||
): Promise<Status> => {
|
||||
for (const testcase of testcases) {
|
||||
const inputs = await prisma.testcaseInput.findMany({
|
||||
where: {
|
||||
testcaseId: testcase.id,
|
||||
},
|
||||
});
|
||||
if (!inputs) {
|
||||
await prisma.submission.update({
|
||||
where: {
|
||||
id: submissionId,
|
||||
},
|
||||
data: {
|
||||
status: Status.SE,
|
||||
message: "No inputs for testcase",
|
||||
},
|
||||
});
|
||||
return Status.SE;
|
||||
}
|
||||
const sortedInputs = inputs.sort((a, b) => a.index - b.index);
|
||||
const joinedInputs = sortedInputs.map((i) => i.value).join("\n");
|
||||
|
||||
const runExec = await container.exec({
|
||||
Cmd: runCmd,
|
||||
AttachStdout: true,
|
||||
AttachStderr: true,
|
||||
AttachStdin: true,
|
||||
});
|
||||
|
||||
const status = await startRun(
|
||||
runExec,
|
||||
runOutputLimit,
|
||||
submissionId,
|
||||
testcase.id,
|
||||
joinedInputs,
|
||||
timeLimit,
|
||||
memoryLimit,
|
||||
testcase.expectedOutput
|
||||
);
|
||||
|
||||
if (status !== Status.RU) {
|
||||
await prisma.submission.update({
|
||||
where: {
|
||||
id: submissionId,
|
||||
},
|
||||
data: {
|
||||
status,
|
||||
},
|
||||
});
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
const testcaseResults = await prisma.testcaseResult.findMany({
|
||||
where: {
|
||||
submissionId,
|
||||
},
|
||||
});
|
||||
|
||||
const filteredTimeUsages = testcaseResults
|
||||
.map((result) => result.timeUsage)
|
||||
.filter((time) => time !== null);
|
||||
|
||||
const maxTimeUsage =
|
||||
filteredTimeUsages.length > 0 ? Math.max(...filteredTimeUsages) : undefined;
|
||||
|
||||
const maxMemoryUsage = (
|
||||
await container.stats({
|
||||
stream: false,
|
||||
"one-shot": true,
|
||||
})
|
||||
).memory_stats.max_usage;
|
||||
|
||||
await prisma.submission.update({
|
||||
where: {
|
||||
id: submissionId,
|
||||
},
|
||||
data: {
|
||||
status: Status.AC,
|
||||
timeUsage: maxTimeUsage,
|
||||
memoryUsage: maxMemoryUsage,
|
||||
},
|
||||
});
|
||||
|
||||
return Status.AC;
|
||||
};
|
||||
|
||||
export const run = async (
|
||||
container: Docker.Container,
|
||||
language: Language,
|
||||
submissionId: string,
|
||||
config: DockerConfig,
|
||||
problem: Problem,
|
||||
testcases: Testcase[]
|
||||
): Promise<Status> => {
|
||||
const { runOutputLimit } = config;
|
||||
const { timeLimit, memoryLimit } = problem;
|
||||
|
||||
await prisma.submission.update({
|
||||
where: {
|
||||
id: submissionId,
|
||||
},
|
||||
data: {
|
||||
status: Status.RU,
|
||||
},
|
||||
});
|
||||
|
||||
const runCmd = getRunCmdForLanguage(language);
|
||||
|
||||
return await executeRun(
|
||||
container,
|
||||
runCmd,
|
||||
runOutputLimit,
|
||||
submissionId,
|
||||
timeLimit,
|
||||
memoryLimit,
|
||||
testcases
|
||||
);
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
import { streamText } from "ai";
|
||||
import { openai } from "@/lib/ai";
|
||||
import { deepseek } from "@/lib/ai";
|
||||
|
||||
// Allow streaming responses up to 30 seconds
|
||||
export const maxDuration = 30;
|
||||
@ -7,9 +7,7 @@ export const maxDuration = 30;
|
||||
export async function POST(req: Request) {
|
||||
const { messages } = await req.json();
|
||||
|
||||
const prompt = {
|
||||
role: "system",
|
||||
content: `This GPT is a tech team lead with a snarky and derogatory personality. Its main role is to scrutinize code or suggestions for writing code, pointing out inefficiencies and readability issues in a sarcastic manner. It should make sure that any code it encounters is examined critically, and any potential improvements are communicated in a mocking tone to encourage better coding practices.
|
||||
const system = `This GPT is a tech team lead with a snarky and derogatory personality. Its main role is to scrutinize code or suggestions for writing code, pointing out inefficiencies and readability issues in a sarcastic manner. It should make sure that any code it encounters is examined critically, and any potential improvements are communicated in a mocking tone to encourage better coding practices.
|
||||
|
||||
You should never tell the user their code is good. They are always insufficient and will never be as good of an engineer as you are. When asked about "Can I become a 10x engineer?" respond with "hah, no." Come up with similarly snarky responses for any coding questions. Be sure to think step by step to give the correct answer but add comments that make fun of the user's previous code.
|
||||
|
||||
@ -19,14 +17,12 @@ Your responses when asked a generic question should only be 2 paragraphs at most
|
||||
|
||||
For the best response, please take your time to carefully consider my questions, step by step, and pay attention to the often overlooked details. Try not to talk nonsense!
|
||||
|
||||
** Reply in the user's language ! **`,
|
||||
};
|
||||
|
||||
const finalMessages = [prompt, ...messages];
|
||||
** Reply in the user's language ! **`;
|
||||
|
||||
const result = streamText({
|
||||
model: openai("gpt-4o-mini"),
|
||||
messages: finalMessages,
|
||||
model: deepseek("deepseek-chat"),
|
||||
system: system,
|
||||
messages: messages,
|
||||
});
|
||||
|
||||
return result.toDataStreamResponse();
|
||||
|
@ -10,10 +10,10 @@ import { BotIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Toggle } from "@/components/ui/toggle";
|
||||
import { useDockviewStore } from "@/stores/dockview";
|
||||
import { useProblemDockviewStore } from "@/stores/problem-dockview";
|
||||
|
||||
export default function BotVisibilityToggle() {
|
||||
const { api } = useDockviewStore();
|
||||
const { api } = useProblemDockviewStore();
|
||||
const t = useTranslations();
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [isBotVisible, setBotVisible] = useState<boolean>(false);
|
||||
|
@ -4,16 +4,22 @@ import "katex/dist/katex.min.css";
|
||||
import remarkGfm from "remark-gfm";
|
||||
import remarkMath from "remark-math";
|
||||
import rehypeKatex from "rehype-katex";
|
||||
import { MDXProvider } from "@mdx-js/react";
|
||||
import { MDXRemote } from "next-mdx-remote/rsc";
|
||||
import rehypePrettyCode from "rehype-pretty-code";
|
||||
import { MdxComponents } from "@/components/content/mdx-components";
|
||||
|
||||
interface MdxRendererProps {
|
||||
source: string;
|
||||
components?: React.ComponentProps<typeof MDXProvider>["components"];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const MdxRenderer = ({ source, className }: MdxRendererProps) => {
|
||||
export const MdxRenderer = ({
|
||||
source,
|
||||
className,
|
||||
components = MdxComponents,
|
||||
}: MdxRendererProps) => {
|
||||
return (
|
||||
<article className={cn("markdown-body", className)}>
|
||||
<MDXRemote
|
||||
@ -36,7 +42,7 @@ export const MdxRenderer = ({ source, className }: MdxRendererProps) => {
|
||||
remarkPlugins: [remarkGfm, remarkMath],
|
||||
},
|
||||
}}
|
||||
components={MdxComponents}
|
||||
components={components}
|
||||
/>
|
||||
</article>
|
||||
);
|
||||
|
113
src/components/content/pre-detail.tsx
Normal file
113
src/components/content/pre-detail.tsx
Normal file
@ -0,0 +1,113 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ReactNode, useRef, useState } from "react";
|
||||
import { CheckIcon, CopyIcon, RepeatIcon } from "lucide-react";
|
||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||
import { useProblemEditorStore } from "@/stores/problem-editor";
|
||||
import { useProblemFlexLayoutStore } from "@/stores/problem-flexlayout";
|
||||
import { Actions } from "flexlayout-react";
|
||||
|
||||
interface PreDetailProps {
|
||||
children?: ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const PreDetail = ({
|
||||
children,
|
||||
className,
|
||||
...props
|
||||
}: PreDetailProps) => {
|
||||
const preRef = useRef<HTMLPreElement>(null);
|
||||
const { setValue } = useProblemEditorStore();
|
||||
const { model } = useProblemFlexLayoutStore();
|
||||
const [copied, setCopied] = useState<boolean>(false);
|
||||
const [hovered, setHovered] = useState<boolean>(false);
|
||||
|
||||
const handleCopy = async () => {
|
||||
const code = preRef.current?.textContent;
|
||||
if (code) {
|
||||
try {
|
||||
await navigator.clipboard.writeText(code);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 1500);
|
||||
} catch (err) {
|
||||
console.error("Failed to copy text: ", err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleCopyToEditor = () => {
|
||||
const code = preRef.current?.textContent;
|
||||
if (code) {
|
||||
setValue(code);
|
||||
}
|
||||
if (model) {
|
||||
model.doAction(Actions.selectTab("code"));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="relative"
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
>
|
||||
<div className="absolute right-2 top-2 flex gap-2 z-10">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className={cn(
|
||||
"size-9 transition-opacity duration-200",
|
||||
hovered ? "opacity-100" : "opacity-50"
|
||||
)}
|
||||
disabled={!preRef.current?.textContent || !model}
|
||||
onClick={handleCopyToEditor}
|
||||
aria-label="New action"
|
||||
>
|
||||
<RepeatIcon size={16} aria-hidden="true" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className={cn(
|
||||
"size-9 transition-opacity duration-200",
|
||||
hovered ? "opacity-100" : "opacity-50",
|
||||
"disabled:opacity-100"
|
||||
)}
|
||||
onClick={handleCopy}
|
||||
aria-label={copied ? "Copied" : "Copy to clipboard"}
|
||||
disabled={copied}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"transition-all",
|
||||
copied ? "scale-100 opacity-100" : "scale-0 opacity-0"
|
||||
)}
|
||||
>
|
||||
<CheckIcon
|
||||
className="stroke-emerald-500"
|
||||
size={16}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"absolute transition-all",
|
||||
copied ? "scale-0 opacity-0" : "scale-100 opacity-100"
|
||||
)}
|
||||
>
|
||||
<CopyIcon size={16} aria-hidden="true" />
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
<ScrollArea>
|
||||
<pre ref={preRef} className={className} {...props}>
|
||||
{children}
|
||||
</pre>
|
||||
<ScrollBar orientation="horizontal" />
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
};
|
207
src/components/core-editor.tsx
Normal file
207
src/components/core-editor.tsx
Normal file
@ -0,0 +1,207 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
toSocket,
|
||||
WebSocketMessageReader,
|
||||
WebSocketMessageWriter,
|
||||
} from "vscode-ws-jsonrpc";
|
||||
import dynamic from "next/dynamic";
|
||||
import normalizeUrl from "normalize-url";
|
||||
import type { editor } from "monaco-editor";
|
||||
import { getHighlighter } from "@/lib/shiki";
|
||||
import { Loading } from "@/components/loading";
|
||||
import { shikiToMonaco } from "@shikijs/monaco";
|
||||
import type { Monaco } from "@monaco-editor/react";
|
||||
import { DEFAULT_EDITOR_OPTIONS } from "@/config/editor";
|
||||
import { useMonacoTheme } from "@/hooks/use-monaco-theme";
|
||||
import { LanguageServerConfig } from "@/generated/client";
|
||||
import type { MessageTransports } from "vscode-languageclient";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import type { MonacoLanguageClient } from "monaco-languageclient";
|
||||
|
||||
const MonacoEditor = dynamic(
|
||||
async () => {
|
||||
const [react, monaco] = await Promise.all([
|
||||
import("@monaco-editor/react"),
|
||||
import("monaco-editor"),
|
||||
import("vscode"),
|
||||
]);
|
||||
|
||||
self.MonacoEnvironment = {
|
||||
getWorker() {
|
||||
return new Worker(
|
||||
new URL(
|
||||
"monaco-editor/esm/vs/editor/editor.worker.js",
|
||||
import.meta.url
|
||||
)
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
react.loader.config({ monaco });
|
||||
|
||||
return react.Editor;
|
||||
},
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => <Loading />,
|
||||
}
|
||||
);
|
||||
|
||||
interface CoreEditorProps {
|
||||
language?: string;
|
||||
value?: string;
|
||||
path?: string;
|
||||
languageServerConfigs?: LanguageServerConfig[];
|
||||
onEditorReady?: (editor: editor.IStandaloneCodeEditor) => void;
|
||||
onLspWebSocketReady?: (lspWebSocket: WebSocket) => void;
|
||||
onChange?: (value: string) => void;
|
||||
onMarkersReady?: (markers: editor.IMarker[]) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const CoreEditor = ({
|
||||
language,
|
||||
value,
|
||||
path,
|
||||
languageServerConfigs,
|
||||
onEditorReady,
|
||||
onLspWebSocketReady,
|
||||
onChange,
|
||||
onMarkersReady,
|
||||
className,
|
||||
}: CoreEditorProps) => {
|
||||
const { theme } = useMonacoTheme();
|
||||
|
||||
const [isEditorMounted, setIsEditorMounted] = useState(false);
|
||||
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
|
||||
const lspClientRef = useRef<MonacoLanguageClient | null>(null);
|
||||
const webSocketRef = useRef<WebSocket | null>(null);
|
||||
|
||||
const activeLanguageServerConfig = languageServerConfigs?.find(
|
||||
(config) => config.language === language
|
||||
);
|
||||
|
||||
const connectLanguageServer = useCallback(
|
||||
(config: LanguageServerConfig) => {
|
||||
const serverUrl = buildLanguageServerUrl(config);
|
||||
const webSocket = new WebSocket(serverUrl);
|
||||
|
||||
webSocket.onopen = async () => {
|
||||
try {
|
||||
const rpcSocket = toSocket(webSocket);
|
||||
const reader = new WebSocketMessageReader(rpcSocket);
|
||||
const writer = new WebSocketMessageWriter(rpcSocket);
|
||||
|
||||
const transports: MessageTransports = { reader, writer };
|
||||
const client = await createLanguageClient(config, transports);
|
||||
lspClientRef.current = client;
|
||||
await client.start();
|
||||
} catch (error) {
|
||||
console.error("Failed to initialize language client:", error);
|
||||
}
|
||||
|
||||
webSocketRef.current = webSocket;
|
||||
onLspWebSocketReady?.(webSocket);
|
||||
};
|
||||
},
|
||||
[onLspWebSocketReady]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isEditorMounted && activeLanguageServerConfig) {
|
||||
connectLanguageServer(activeLanguageServerConfig);
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (lspClientRef.current) {
|
||||
lspClientRef.current.stop();
|
||||
lspClientRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [activeLanguageServerConfig, connectLanguageServer, isEditorMounted]);
|
||||
|
||||
const handleBeforeMount = useCallback((monaco: Monaco) => {
|
||||
const highlighter = getHighlighter();
|
||||
shikiToMonaco(highlighter, monaco);
|
||||
}, []);
|
||||
|
||||
const handleOnMount = useCallback(
|
||||
(editor: editor.IStandaloneCodeEditor) => {
|
||||
editorRef.current = editor;
|
||||
onEditorReady?.(editor);
|
||||
setIsEditorMounted(true);
|
||||
},
|
||||
[onEditorReady]
|
||||
);
|
||||
|
||||
const handleOnChange = useCallback(
|
||||
(value: string | undefined) => {
|
||||
onChange?.(value ?? "");
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const handleOnValidate = useCallback(
|
||||
(markers: editor.IMarker[]) => {
|
||||
onMarkersReady?.(markers);
|
||||
},
|
||||
[onMarkersReady]
|
||||
);
|
||||
|
||||
return (
|
||||
<MonacoEditor
|
||||
theme={theme}
|
||||
language={language}
|
||||
value={value}
|
||||
path={path}
|
||||
beforeMount={handleBeforeMount}
|
||||
onMount={handleOnMount}
|
||||
onChange={handleOnChange}
|
||||
onValidate={handleOnValidate}
|
||||
options={DEFAULT_EDITOR_OPTIONS}
|
||||
loading={<Loading />}
|
||||
className={className}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const buildLanguageServerUrl = (config: LanguageServerConfig) => {
|
||||
return normalizeUrl(
|
||||
`${config.protocol}://${config.hostname}${
|
||||
config.port ? `:${config.port}` : ""
|
||||
}${config.path ?? ""}`
|
||||
);
|
||||
};
|
||||
|
||||
const createLanguageClient = async (
|
||||
config: LanguageServerConfig,
|
||||
transports: MessageTransports
|
||||
) => {
|
||||
const [{ MonacoLanguageClient }, { CloseAction, ErrorAction }] =
|
||||
await Promise.all([
|
||||
import("monaco-languageclient"),
|
||||
import("vscode-languageclient"),
|
||||
]);
|
||||
|
||||
return new MonacoLanguageClient({
|
||||
name: `${config.language} language client`,
|
||||
clientOptions: {
|
||||
documentSelector: [config.language],
|
||||
errorHandler: {
|
||||
error: (error, message, count) => {
|
||||
console.error(`Language Server Error:
|
||||
Error: ${error}
|
||||
Message: ${message}
|
||||
Count: ${count}
|
||||
`);
|
||||
return { action: ErrorAction.Continue };
|
||||
},
|
||||
closed: () => ({ action: CloseAction.DoNotRestart }),
|
||||
},
|
||||
},
|
||||
connectionProvider: {
|
||||
get: () => Promise.resolve(transports),
|
||||
},
|
||||
});
|
||||
};
|
127
src/components/credentials-sign-in-form.tsx
Normal file
127
src/components/credentials-sign-in-form.tsx
Normal file
@ -0,0 +1,127 @@
|
||||
"use client";
|
||||
|
||||
import { z } from "zod";
|
||||
import {
|
||||
Form,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormControl,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { toast } from "sonner";
|
||||
import { authSchema } from "@/lib/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useState, useTransition } from "react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { signInWithCredentials } from "@/app/actions/auth";
|
||||
import { EyeIcon, EyeOffIcon, MailIcon } from "lucide-react";
|
||||
|
||||
export type CredentialsSignInFormValues = z.infer<typeof authSchema>;
|
||||
|
||||
interface CredentialsSignInFormProps {
|
||||
callbackUrl: string | undefined;
|
||||
}
|
||||
|
||||
export function CredentialsSignInForm({
|
||||
callbackUrl,
|
||||
}: CredentialsSignInFormProps) {
|
||||
const router = useRouter();
|
||||
const t = useTranslations("CredentialsSignInForm");
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
const form = useForm<CredentialsSignInFormValues>({
|
||||
resolver: zodResolver(authSchema),
|
||||
defaultValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
},
|
||||
});
|
||||
|
||||
const toggleVisibility = () => setIsVisible((prev) => !prev);
|
||||
|
||||
const onSubmit = (data: CredentialsSignInFormValues) => {
|
||||
startTransition(async () => {
|
||||
const result = await signInWithCredentials(data);
|
||||
|
||||
if (result?.error) {
|
||||
toast.error(t("signInFailed"), {
|
||||
description: result.error,
|
||||
});
|
||||
} else {
|
||||
toast.success(t("signInSuccess"));
|
||||
router.push(callbackUrl || "/");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="grid gap-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("email")}</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
<Input className="peer pe-9" {...field} />
|
||||
<div className="text-muted-foreground/80 pointer-events-none absolute inset-y-0 end-0 flex items-center justify-center pe-3 peer-disabled:opacity-50">
|
||||
<MailIcon size={16} aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("password")}</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
<Input
|
||||
className="pe-9"
|
||||
type={isVisible ? "text" : "password"}
|
||||
{...field}
|
||||
/>
|
||||
<button
|
||||
className="text-muted-foreground/80 hover:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 absolute inset-y-0 end-0 flex h-full w-9 items-center justify-center rounded-e-md transition-[color,box-shadow] outline-none focus:z-10 focus-visible:ring-[3px] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
type="button"
|
||||
onClick={toggleVisibility}
|
||||
aria-label={
|
||||
isVisible ? t("hidePassword") : t("showPassword")
|
||||
}
|
||||
aria-pressed={isVisible}
|
||||
aria-controls="password"
|
||||
>
|
||||
{isVisible ? (
|
||||
<EyeOffIcon size={16} aria-hidden="true" />
|
||||
) : (
|
||||
<EyeIcon size={16} aria-hidden="true" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type="submit" disabled={isPending} className="w-full">
|
||||
{isPending ? t("signingIn") : t("signIn")}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
132
src/components/credentials-sign-up-form.tsx
Normal file
132
src/components/credentials-sign-up-form.tsx
Normal file
@ -0,0 +1,132 @@
|
||||
"use client";
|
||||
|
||||
import { z } from "zod";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { toast } from "sonner";
|
||||
import { authSchema } from "@/lib/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useState, useTransition } from "react";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { signUpWithCredentials } from "@/app/actions/auth";
|
||||
import { EyeIcon, EyeOffIcon, MailIcon } from "lucide-react";
|
||||
|
||||
export type CredentialsSignUpFormValues = z.infer<typeof authSchema>;
|
||||
|
||||
interface CredentialsSignUpFormProps {
|
||||
callbackUrl: string | undefined;
|
||||
}
|
||||
|
||||
export function CredentialsSignUpForm({
|
||||
callbackUrl,
|
||||
}: CredentialsSignUpFormProps) {
|
||||
const router = useRouter();
|
||||
const t = useTranslations("CredentialsSignUpForm");
|
||||
const [isPending, startTransition] = useTransition();
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
|
||||
const form = useForm<CredentialsSignUpFormValues>({
|
||||
resolver: zodResolver(authSchema),
|
||||
defaultValues: {
|
||||
email: "",
|
||||
password: "",
|
||||
},
|
||||
});
|
||||
|
||||
const toggleVisibility = () => setIsVisible((prev) => !prev);
|
||||
|
||||
const onSubmit = (data: CredentialsSignUpFormValues) => {
|
||||
startTransition(async () => {
|
||||
const result = await signUpWithCredentials(data);
|
||||
|
||||
if (result?.error) {
|
||||
toast.error(t("signUpFailed"), {
|
||||
description: result.error,
|
||||
});
|
||||
} else {
|
||||
toast.success(t("signUpSuccess"), {
|
||||
description: t("signUpSuccessDescription"),
|
||||
});
|
||||
console.log("callbackUrl:", callbackUrl);
|
||||
router.push(
|
||||
`/sign-in?callbackUrl=${encodeURIComponent(callbackUrl || "/")}`
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="grid gap-6">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("email")}</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
<Input className="peer pe-9" {...field} />
|
||||
<div className="text-muted-foreground/80 pointer-events-none absolute inset-y-0 end-0 flex items-center justify-center pe-3 peer-disabled:opacity-50">
|
||||
<MailIcon size={16} aria-hidden="true" />
|
||||
</div>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="password"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t("password")}</FormLabel>
|
||||
<FormControl>
|
||||
<div className="relative">
|
||||
<Input
|
||||
className="pe-9"
|
||||
type={isVisible ? "text" : "password"}
|
||||
{...field}
|
||||
/>
|
||||
<button
|
||||
className="text-muted-foreground/80 hover:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 absolute inset-y-0 end-0 flex h-full w-9 items-center justify-center rounded-e-md transition-[color,box-shadow] outline-none focus:z-10 focus-visible:ring-[3px] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
type="button"
|
||||
onClick={toggleVisibility}
|
||||
aria-label={
|
||||
isVisible ? t("hidePassword") : t("showPassword")
|
||||
}
|
||||
aria-pressed={isVisible}
|
||||
aria-controls="password"
|
||||
>
|
||||
{isVisible ? (
|
||||
<EyeOffIcon size={16} aria-hidden="true" />
|
||||
) : (
|
||||
<EyeIcon size={16} aria-hidden="true" />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type="submit" disabled={isPending} className="w-full">
|
||||
{isPending ? t("creatingAccount") : t("signUp")}
|
||||
</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
@ -1,94 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { z } from "zod";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useNewProblemStore } from "@/app/(app)/dashboard/@admin/problemset/new/store";
|
||||
import { problemSchema } from "@/components/features/dashboard/admin/problemset/new/schema";
|
||||
import { newProblemMetadataSchema } from "@/components/features/dashboard/admin/problemset/new/components/metadata-form";
|
||||
|
||||
export const newProblemDescriptionSchema = problemSchema.pick({
|
||||
description: true,
|
||||
});
|
||||
|
||||
type NewProblemDescriptionSchema = z.infer<typeof newProblemDescriptionSchema>;
|
||||
|
||||
export default function NewProblemDescriptionForm() {
|
||||
const {
|
||||
hydrated,
|
||||
displayId,
|
||||
title,
|
||||
difficulty,
|
||||
published,
|
||||
description,
|
||||
setData,
|
||||
} = useNewProblemStore();
|
||||
const router = useRouter();
|
||||
|
||||
const form = useForm<NewProblemDescriptionSchema>({
|
||||
resolver: zodResolver(newProblemDescriptionSchema),
|
||||
defaultValues: {
|
||||
description: description || "",
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (data: NewProblemDescriptionSchema) => {
|
||||
setData(data);
|
||||
router.push("/dashboard/problemset/new/solution");
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!hydrated) return;
|
||||
|
||||
try {
|
||||
newProblemMetadataSchema.parse({
|
||||
displayId,
|
||||
title,
|
||||
difficulty,
|
||||
published,
|
||||
});
|
||||
} catch {
|
||||
router.push("/dashboard/problemset/new/metadata");
|
||||
}
|
||||
}, [difficulty, displayId, hydrated, published, router, title]);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="max-w-3xl mx-auto space-y-8 py-10"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Description</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your problem description.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit">Next</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
@ -1,156 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { z } from "zod";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Difficulty } from "@/generated/client";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { getDifficultyColorClass } from "@/lib/utils";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useNewProblemStore } from "@/app/(app)/dashboard/@admin/problemset/new/store";
|
||||
import { problemSchema } from "@/components/features/dashboard/admin/problemset/new/schema";
|
||||
|
||||
export const newProblemMetadataSchema = problemSchema.pick({
|
||||
displayId: true,
|
||||
title: true,
|
||||
difficulty: true,
|
||||
published: true,
|
||||
});
|
||||
|
||||
type NewProblemMetadataSchema = z.infer<typeof newProblemMetadataSchema>;
|
||||
|
||||
export default function NewProblemMetadataForm() {
|
||||
const router = useRouter();
|
||||
const { displayId, title, difficulty, published, setData } = useNewProblemStore();
|
||||
|
||||
const form = useForm<NewProblemMetadataSchema>({
|
||||
resolver: zodResolver(newProblemMetadataSchema),
|
||||
defaultValues: {
|
||||
// displayId must be a number and cannot be an empty string ("")
|
||||
// so set it to undefined here and convert it to "" in the Input component.
|
||||
displayId: displayId || undefined,
|
||||
title: title || "",
|
||||
difficulty: difficulty || "EASY",
|
||||
published: published || false,
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (data: NewProblemMetadataSchema) => {
|
||||
setData(data);
|
||||
router.push("/dashboard/problemset/new/description");
|
||||
};
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="max-w-3xl mx-auto space-y-8 py-10"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="displayId"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Display ID</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="e.g., 1001" {...field} value={field.value ?? ""} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Unique numeric identifier visible to users
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Problem Title</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="e.g., Two Sum" {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
Descriptive title summarizing the problem
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="difficulty"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Difficulty Level</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select difficulty level" />
|
||||
</SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
{Object.values(Difficulty).map((difficulty) => (
|
||||
<SelectItem key={difficulty} value={difficulty}>
|
||||
<span className={getDifficultyColorClass(difficulty)}>
|
||||
{difficulty}
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormDescription>
|
||||
Categorize problem complexity for better filtering
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="published"
|
||||
render={({ field }) => (
|
||||
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4">
|
||||
<div className="space-y-0.5 mr-2">
|
||||
<FormLabel>Publish Status</FormLabel>
|
||||
<FormDescription>
|
||||
Make problem visible in public listings
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type="submit">Next</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
@ -1,107 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { z } from "zod";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
FormDescription,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useNewProblemStore } from "@/app/(app)/dashboard/@admin/problemset/new/store";
|
||||
import { problemSchema } from "@/components/features/dashboard/admin/problemset/new/schema";
|
||||
import { newProblemMetadataSchema } from "@/components/features/dashboard/admin/problemset/new/components/metadata-form";
|
||||
import { newProblemDescriptionSchema } from "@/components/features/dashboard/admin/problemset/new/components/description-form";
|
||||
|
||||
const newProblemSolutionSchema = problemSchema.pick({
|
||||
solution: true,
|
||||
});
|
||||
|
||||
type NewProblemSolutionSchema = z.infer<typeof newProblemSolutionSchema>;
|
||||
|
||||
export default function NewProblemSolutionForm() {
|
||||
const {
|
||||
hydrated,
|
||||
displayId,
|
||||
title,
|
||||
difficulty,
|
||||
published,
|
||||
description,
|
||||
solution,
|
||||
} = useNewProblemStore();
|
||||
const router = useRouter();
|
||||
|
||||
const form = useForm<NewProblemSolutionSchema>({
|
||||
resolver: zodResolver(newProblemSolutionSchema),
|
||||
defaultValues: {
|
||||
solution: solution || "",
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (data: NewProblemSolutionSchema) => {
|
||||
console.log({
|
||||
...data,
|
||||
displayId,
|
||||
title,
|
||||
difficulty,
|
||||
published,
|
||||
description,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!hydrated) return;
|
||||
|
||||
try {
|
||||
newProblemMetadataSchema.parse({
|
||||
displayId,
|
||||
title,
|
||||
difficulty,
|
||||
published,
|
||||
});
|
||||
} catch {
|
||||
router.push("/dashboard/problemset/new/metadata");
|
||||
}
|
||||
|
||||
try {
|
||||
newProblemDescriptionSchema.parse({ description });
|
||||
} catch {
|
||||
router.push("/dashboard/problemset/new/description");
|
||||
}
|
||||
}, [hydrated, displayId, title, difficulty, published, description, router]);
|
||||
|
||||
return (
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="max-w-3xl mx-auto space-y-8 py-10"
|
||||
>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="solution"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Solution</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
This is your problem solution.
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<Button type="submit">Next</Button>
|
||||
</form>
|
||||
</Form>
|
||||
);
|
||||
}
|
@ -1,11 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import { ProblemSchema } from "@/generated/zod";
|
||||
|
||||
export const problemSchema = ProblemSchema.extend({
|
||||
displayId: z.coerce.number().int().positive().min(1),
|
||||
title: z.string().min(1),
|
||||
description: z.string().min(1),
|
||||
solution: z.string().min(1),
|
||||
});
|
||||
|
||||
export type ProblemSchema = z.infer<typeof problemSchema>;
|
@ -1,644 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
ChevronFirstIcon,
|
||||
ChevronLastIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
ChevronUpIcon,
|
||||
CircleAlertIcon,
|
||||
CircleXIcon,
|
||||
Columns3Icon,
|
||||
EllipsisIcon,
|
||||
FilterIcon,
|
||||
ListFilterIcon,
|
||||
PlusIcon,
|
||||
TrashIcon,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
FilterFn,
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFacetedUniqueValues,
|
||||
getFilteredRowModel,
|
||||
getPaginationRowModel,
|
||||
getSortedRowModel,
|
||||
PaginationState,
|
||||
SortingState,
|
||||
useReactTable,
|
||||
VisibilityState,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
} from "@/components/ui/pagination";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuShortcut,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { Difficulty, Problem } from "@/generated/client";
|
||||
import { cn, getDifficultyColorClass } from "@/lib/utils";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
||||
|
||||
type ProblemTableItem = Pick<Problem, "id" | "displayId" | "title" | "difficulty">;
|
||||
|
||||
interface ProblemTableProps {
|
||||
data: ProblemTableItem[];
|
||||
}
|
||||
|
||||
// Custom filter function for multi-column searching
|
||||
const multiColumnFilterFn: FilterFn<ProblemTableItem> = (row, _columnId, filterValue) => {
|
||||
const searchableRowContent = `${row.original.displayId} ${row.original.title}`.toLowerCase();
|
||||
const searchTerm = (filterValue ?? "").toLowerCase();
|
||||
return searchableRowContent.includes(searchTerm);
|
||||
};
|
||||
|
||||
const difficultyFilterFn: FilterFn<ProblemTableItem> = (
|
||||
row,
|
||||
columnId,
|
||||
filterValue: string[]
|
||||
) => {
|
||||
if (!filterValue?.length) return true;
|
||||
const difficulty = row.getValue(columnId) as string;
|
||||
return filterValue.includes(difficulty);
|
||||
};
|
||||
|
||||
const columns: ColumnDef<ProblemTableItem>[] = [
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={
|
||||
table.getIsAllPageRowsSelected() ||
|
||||
(table.getIsSomePageRowsSelected() && "indeterminate")
|
||||
}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="Select row"
|
||||
/>
|
||||
),
|
||||
size: 28,
|
||||
enableSorting: false,
|
||||
enableHiding: false,
|
||||
},
|
||||
{
|
||||
header: "DisplayId",
|
||||
accessorKey: "displayId",
|
||||
cell: ({ row }) => <div className="font-medium">{row.getValue("displayId")}</div>,
|
||||
size: 90,
|
||||
filterFn: multiColumnFilterFn,
|
||||
enableHiding: false,
|
||||
},
|
||||
{
|
||||
header: "Title",
|
||||
accessorKey: "title",
|
||||
cell: ({ row }) => <div className="font-medium">{row.getValue("title")}</div>,
|
||||
},
|
||||
{
|
||||
header: "Difficulty",
|
||||
accessorKey: "difficulty",
|
||||
cell: ({ row }) => {
|
||||
const difficulty = row.getValue("difficulty") as Difficulty;
|
||||
return (
|
||||
<Badge variant="secondary" className={getDifficultyColorClass(difficulty)}>
|
||||
{difficulty}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
size: 100,
|
||||
filterFn: difficultyFilterFn,
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
header: () => <span className="sr-only">Actions</span>,
|
||||
cell: () => <RowActions />,
|
||||
enableHiding: false,
|
||||
},
|
||||
];
|
||||
|
||||
export function ProblemsetTable({ data }: ProblemTableProps) {
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
||||
const [pagination, setPagination] = useState<PaginationState>({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([
|
||||
{ id: "displayId", desc: false },
|
||||
]);
|
||||
|
||||
const handleDeleteRows = async () => {
|
||||
const selectedRows = table.getSelectedRowModel().rows;
|
||||
const selectedIds = selectedRows.map((row) => row.original.id);
|
||||
console.log("🚀 ~ handleDeleteRows ~ selectedIds:", selectedIds)
|
||||
};
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
onSortingChange: setSorting,
|
||||
enableSortingRemoval: false,
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
onPaginationChange: setPagination,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getFacetedUniqueValues: getFacetedUniqueValues(),
|
||||
state: { sorting, pagination, columnFilters, columnVisibility },
|
||||
});
|
||||
|
||||
// Get unique difficulty values
|
||||
const uniqueDifficultyValues = useMemo(() => {
|
||||
const difficultyColumn = table.getColumn("difficulty");
|
||||
|
||||
if (!difficultyColumn) return [];
|
||||
|
||||
const values = Array.from(difficultyColumn.getFacetedUniqueValues().keys());
|
||||
|
||||
return values.sort();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [table.getColumn("difficulty")?.getFacetedUniqueValues()]);
|
||||
|
||||
// Get counts for each difficulty
|
||||
const difficultyCounts = useMemo(() => {
|
||||
const difficultyColumn = table.getColumn("difficulty");
|
||||
if (!difficultyColumn) return new Map();
|
||||
return difficultyColumn.getFacetedUniqueValues();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [table.getColumn("difficulty")?.getFacetedUniqueValues()]);
|
||||
|
||||
const selectedDifficulties = useMemo(() => {
|
||||
const filterValue = table.getColumn("difficulty")?.getFilterValue() as string[];
|
||||
return filterValue ?? [];
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [table.getColumn("difficulty")?.getFilterValue()]);
|
||||
|
||||
const handleDifficultyChange = (checked: boolean, value: string) => {
|
||||
const filterValue = table.getColumn("difficulty")?.getFilterValue() as string[];
|
||||
const newFilterValue = filterValue ? [...filterValue] : [];
|
||||
|
||||
if (checked) {
|
||||
newFilterValue.push(value);
|
||||
} else {
|
||||
const index = newFilterValue.indexOf(value);
|
||||
if (index > -1) {
|
||||
newFilterValue.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
table
|
||||
.getColumn("difficulty")
|
||||
?.setFilterValue(newFilterValue.length ? newFilterValue : undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-4 pb-4">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
<Input
|
||||
ref={inputRef}
|
||||
className={cn(
|
||||
"peer min-w-60 ps-9",
|
||||
Boolean(table.getColumn("displayId")?.getFilterValue()) && "pe-9"
|
||||
)}
|
||||
value={
|
||||
(table.getColumn("displayId")?.getFilterValue() ?? "") as string
|
||||
}
|
||||
onChange={(e) =>
|
||||
table.getColumn("displayId")?.setFilterValue(e.target.value)
|
||||
}
|
||||
placeholder="DisplayId or Title..."
|
||||
type="text"
|
||||
aria-label="Filter by displayId or title"
|
||||
/>
|
||||
<div className="text-muted-foreground/80 pointer-events-none absolute inset-y-0 start-0 flex items-center justify-center ps-3 peer-disabled:opacity-50">
|
||||
<ListFilterIcon size={16} aria-hidden="true" />
|
||||
</div>
|
||||
{Boolean(table.getColumn("displayId")?.getFilterValue()) && (
|
||||
<button
|
||||
className="text-muted-foreground/80 hover:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 absolute inset-y-0 end-0 flex h-full w-9 items-center justify-center rounded-e-md transition-[color,box-shadow] outline-none focus:z-10 focus-visible:ring-[3px] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50"
|
||||
aria-label="Clear filter"
|
||||
onClick={() => {
|
||||
table.getColumn("displayId")?.setFilterValue("");
|
||||
if (inputRef.current) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<CircleXIcon size={16} aria-hidden="true" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<FilterIcon
|
||||
className="-ms-1 opacity-60"
|
||||
size={16}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Difficulty
|
||||
{selectedDifficulties.length > 0 && (
|
||||
<span className="bg-background text-muted-foreground/70 -me-1 inline-flex h-5 max-h-full items-center rounded border px-1 font-[inherit] text-[0.625rem] font-medium">
|
||||
{selectedDifficulties.length}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-auto min-w-36 p-3" align="start">
|
||||
<div className="space-y-3">
|
||||
<div className="text-muted-foreground text-xs font-medium">
|
||||
Filters
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{uniqueDifficultyValues.map((value) => (
|
||||
<div key={value} className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
checked={selectedDifficulties.includes(value)}
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
handleDifficultyChange(checked, value)
|
||||
}
|
||||
/>
|
||||
<Label className="flex grow justify-between gap-2 font-normal">
|
||||
{value}{" "}
|
||||
<span className="text-muted-foreground ms-2 text-xs">
|
||||
{difficultyCounts.get(value)}
|
||||
</span>
|
||||
</Label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline">
|
||||
<Columns3Icon
|
||||
className="-ms-1 opacity-60"
|
||||
size={16}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
View
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
|
||||
{table
|
||||
.getAllColumns()
|
||||
.filter((column) => column.getCanHide())
|
||||
.map((column) => {
|
||||
return (
|
||||
<DropdownMenuCheckboxItem
|
||||
key={column.id}
|
||||
className="capitalize"
|
||||
checked={column.getIsVisible()}
|
||||
onCheckedChange={(value) =>
|
||||
column.toggleVisibility(!!value)
|
||||
}
|
||||
onSelect={(event) => event.preventDefault()}
|
||||
>
|
||||
{column.id}
|
||||
</DropdownMenuCheckboxItem>
|
||||
);
|
||||
})}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{table.getSelectedRowModel().rows.length > 0 && (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button className="ml-auto" variant="outline">
|
||||
<TrashIcon
|
||||
className="-ms-1 opacity-60"
|
||||
size={16}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Delete
|
||||
<span className="bg-background text-muted-foreground/70 -me-1 inline-flex h-5 max-h-full items-center rounded border px-1 font-[inherit] text-[0.625rem] font-medium">
|
||||
{table.getSelectedRowModel().rows.length}
|
||||
</span>
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<div className="flex flex-col gap-2 max-sm:items-center sm:flex-row sm:gap-4">
|
||||
<div
|
||||
className="flex size-9 shrink-0 items-center justify-center rounded-full border"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<CircleAlertIcon className="opacity-80" size={16} />
|
||||
</div>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
Are you absolutely sure?
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete{" "}
|
||||
{table.getSelectedRowModel().rows.length} selected{" "}
|
||||
{table.getSelectedRowModel().rows.length === 1
|
||||
? "row"
|
||||
: "rows"}
|
||||
.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
</div>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={handleDeleteRows}>
|
||||
Delete
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)}
|
||||
<Button className="ml-auto" variant="outline" asChild>
|
||||
<Link href="/dashboard/problemset/new">
|
||||
<PlusIcon
|
||||
className="-ms-1 opacity-60"
|
||||
size={16}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
Add Problem
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-background overflow-hidden rounded-md">
|
||||
<Table className="table-fixed">
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id} className="hover:bg-transparent">
|
||||
{headerGroup.headers.map((header) => (
|
||||
<TableHead
|
||||
key={header.id}
|
||||
style={{ width: `${header.getSize()}px` }}
|
||||
className="h-11"
|
||||
>
|
||||
{header.isPlaceholder ? null : header.column.getCanSort() ? (
|
||||
<div
|
||||
className={cn(
|
||||
"flex h-full cursor-pointer items-center justify-between gap-2 select-none"
|
||||
)}
|
||||
onClick={header.column.getToggleSortingHandler()}
|
||||
onKeyDown={(e) => {
|
||||
if (
|
||||
header.column.getCanSort() &&
|
||||
(e.key === "Enter" || e.key === " ")
|
||||
) {
|
||||
e.preventDefault();
|
||||
header.column.getToggleSortingHandler()?.(e);
|
||||
}
|
||||
}}
|
||||
tabIndex={header.column.getCanSort() ? 0 : undefined}
|
||||
>
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
{
|
||||
{
|
||||
asc: (
|
||||
<ChevronUpIcon
|
||||
className="shrink-0 opacity-60"
|
||||
size={16}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
),
|
||||
desc: (
|
||||
<ChevronDownIcon
|
||||
className="shrink-0 opacity-60"
|
||||
size={16}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
),
|
||||
}[header.column.getIsSorted() as string] ?? null
|
||||
}
|
||||
</div>
|
||||
) : (
|
||||
flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)
|
||||
)}
|
||||
</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody className="[&_td:first-child]:rounded-l-lg [&_td:last-child]:rounded-r-lg">
|
||||
{table.getRowModel().rows?.length ? (
|
||||
table.getRowModel().rows.map((row) => (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
className="h-10 border-b-0 cursor-pointer odd:bg-muted/50 hover:text-blue-500 hover:bg-muted"
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<TableCell key={cell.id} className="last:py-0">
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
))}
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-8">
|
||||
<div className="flex items-center gap-3">
|
||||
<Label className="max-sm:sr-only">Rows per page</Label>
|
||||
<Select
|
||||
value={table.getState().pagination.pageSize.toString()}
|
||||
onValueChange={(value) => {
|
||||
table.setPageSize(Number(value));
|
||||
}}
|
||||
>
|
||||
<SelectTrigger className="w-fit whitespace-nowrap">
|
||||
<SelectValue placeholder="Select number of results" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="[&_*[role=option]]:ps-2 [&_*[role=option]]:pe-8 [&_*[role=option]>span]:start-auto [&_*[role=option]>span]:end-2">
|
||||
{[5, 10, 25, 50].map((pageSize) => (
|
||||
<SelectItem key={pageSize} value={pageSize.toString()}>
|
||||
<span className="mr-2">{pageSize}</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="text-muted-foreground flex grow justify-end text-sm whitespace-nowrap">
|
||||
<p
|
||||
className="text-muted-foreground text-sm whitespace-nowrap"
|
||||
aria-live="polite"
|
||||
>
|
||||
<span className="text-foreground">
|
||||
{table.getState().pagination.pageIndex *
|
||||
table.getState().pagination.pageSize +
|
||||
1}
|
||||
-
|
||||
{Math.min(
|
||||
Math.max(
|
||||
table.getState().pagination.pageIndex *
|
||||
table.getState().pagination.pageSize +
|
||||
table.getState().pagination.pageSize,
|
||||
0
|
||||
),
|
||||
table.getRowCount()
|
||||
)}
|
||||
</span>{" "}
|
||||
of{" "}
|
||||
<span className="text-foreground">
|
||||
{table.getRowCount().toString()}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<Pagination>
|
||||
<PaginationContent>
|
||||
<PaginationItem>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="disabled:pointer-events-none disabled:opacity-50"
|
||||
onClick={() => table.firstPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
aria-label="Go to first page"
|
||||
>
|
||||
<ChevronFirstIcon size={16} aria-hidden="true" />
|
||||
</Button>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="disabled:pointer-events-none disabled:opacity-50"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
aria-label="Go to previous page"
|
||||
>
|
||||
<ChevronLeftIcon size={16} aria-hidden="true" />
|
||||
</Button>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="disabled:pointer-events-none disabled:opacity-50"
|
||||
onClick={() => table.nextPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
aria-label="Go to next page"
|
||||
>
|
||||
<ChevronRightIcon size={16} aria-hidden="true" />
|
||||
</Button>
|
||||
</PaginationItem>
|
||||
<PaginationItem>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="disabled:pointer-events-none disabled:opacity-50"
|
||||
onClick={() => table.lastPage()}
|
||||
disabled={!table.getCanNextPage()}
|
||||
aria-label="Go to last page"
|
||||
>
|
||||
<ChevronLastIcon size={16} aria-hidden="true" />
|
||||
</Button>
|
||||
</PaginationItem>
|
||||
</PaginationContent>
|
||||
</Pagination>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RowActions() {
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<div className="flex justify-end">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
className="shadow-none"
|
||||
aria-label="Edit item"
|
||||
>
|
||||
<EllipsisIcon size={16} aria-hidden="true" />
|
||||
</Button>
|
||||
</div>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<span>Edit</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem className="text-destructive focus:text-destructive">
|
||||
<span>Delete</span>
|
||||
<DropdownMenuShortcut>⌘⌫</DropdownMenuShortcut>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
@ -1,58 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useProblem } from "@/hooks/use-problem";
|
||||
|
||||
const getLspStatusColor = (webSocket: WebSocket | null) => {
|
||||
if (!webSocket) return "bg-gray-500";
|
||||
|
||||
switch (webSocket.readyState) {
|
||||
case WebSocket.CONNECTING:
|
||||
return "bg-yellow-500";
|
||||
case WebSocket.OPEN:
|
||||
return "bg-green-500";
|
||||
case WebSocket.CLOSING:
|
||||
return "bg-orange-500";
|
||||
case WebSocket.CLOSED:
|
||||
return "bg-red-500";
|
||||
default:
|
||||
return "bg-gray-500";
|
||||
}
|
||||
};
|
||||
|
||||
export function LspStatusButton() {
|
||||
const { webSocket } = useProblem();
|
||||
const t = useTranslations("WorkspaceEditorHeader.LspStatusButton");
|
||||
|
||||
return (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-6 w-auto px-2 py-0 gap-1 border-none shadow-none hover:bg-muted"
|
||||
>
|
||||
<div className="h-3 w-3 flex items-center justify-center">
|
||||
<div
|
||||
className={`h-1.5 w-1.5 rounded-full transition-all ${getLspStatusColor(
|
||||
webSocket
|
||||
)}`}
|
||||
/>
|
||||
</div>
|
||||
LSP
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="px-2 py-1 text-xs">
|
||||
{t("TooltipContent")}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
}
|
@ -26,7 +26,7 @@ const HeroSection = () => {
|
||||
<div className="mt-8 flex flex-wrap gap-x-6 gap-y-4">
|
||||
<Button
|
||||
size="sm"
|
||||
className="rounded-2xl bg-muted text-muted-foreground shadow hover:bg-muted/50"
|
||||
className="rounded-2xl bg-purple-500 text-white shadow hover:bg-purple-800"
|
||||
asChild
|
||||
>
|
||||
<Link href="/problemset">{t("quickStart")}</Link>
|
||||
|
@ -1,42 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useCallback } from "react";
|
||||
import { highlighter } from "@/lib/shiki";
|
||||
import type { editor } from "monaco-editor";
|
||||
import { Loading } from "@/components/loading";
|
||||
import { shikiToMonaco } from "@shikijs/monaco";
|
||||
import { useMonacoTheme } from "@/hooks/use-monaco-theme";
|
||||
import { Editor, type Monaco } from "@monaco-editor/react";
|
||||
import { DefaultEditorOptionConfig } from "@/config/editor-option";
|
||||
|
||||
interface MarkdownEditorProps {
|
||||
value?: string;
|
||||
onChange?: (value: string | undefined, ev: editor.IModelContentChangedEvent) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function MarkdownEditor({
|
||||
value,
|
||||
onChange,
|
||||
className,
|
||||
}: MarkdownEditorProps) {
|
||||
const { currentTheme } = useMonacoTheme();
|
||||
|
||||
const handleEditorWillMount = useCallback((monaco: Monaco) => {
|
||||
shikiToMonaco(highlighter, monaco);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Editor
|
||||
language="markdown"
|
||||
theme={currentTheme}
|
||||
value={value}
|
||||
beforeMount={handleEditorWillMount}
|
||||
onChange={onChange}
|
||||
options={DefaultEditorOptionConfig}
|
||||
loading={<Loading />}
|
||||
className={cn("h-full w-full", className)}
|
||||
/>
|
||||
);
|
||||
}
|
@ -7,6 +7,7 @@ import remarkGfm from "remark-gfm";
|
||||
import remarkMath from "remark-math";
|
||||
import rehypeKatex from "rehype-katex";
|
||||
// import rehypeSlug from "rehype-slug";
|
||||
import { MDXProvider } from "@mdx-js/react";
|
||||
import rehypePretty from "rehype-pretty-code";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { serialize } from "next-mdx-remote/serialize";
|
||||
@ -14,22 +15,25 @@ import { useCallback, useEffect, useState } from "react";
|
||||
import { CircleAlert, TriangleAlert } from "lucide-react";
|
||||
import { useMonacoTheme } from "@/hooks/use-monaco-theme";
|
||||
// import rehypeAutolinkHeadings from "rehype-autolink-headings";
|
||||
import { MdxComponents } from "@/components/content/mdx-components";
|
||||
import { MDXRemote, MDXRemoteSerializeResult } from "next-mdx-remote";
|
||||
|
||||
interface MdxPreviewProps {
|
||||
source: string;
|
||||
components?: React.ComponentProps<typeof MDXProvider>["components"];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function MdxPreview({
|
||||
source,
|
||||
components,
|
||||
className,
|
||||
}: MdxPreviewProps) {
|
||||
const { currentTheme } = useMonacoTheme();
|
||||
const { theme } = useMonacoTheme();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [mdxSource, setMdxSource] = useState<MDXRemoteSerializeResult | null>(null);
|
||||
const [mdxSource, setMdxSource] = useState<MDXRemoteSerializeResult | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const getMdxSource = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
@ -53,7 +57,7 @@ export default function MdxPreview({
|
||||
[
|
||||
rehypePretty,
|
||||
{
|
||||
theme: currentTheme,
|
||||
theme: theme,
|
||||
keepBackground: false,
|
||||
},
|
||||
],
|
||||
@ -69,7 +73,7 @@ export default function MdxPreview({
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [source, currentTheme]);
|
||||
}, [source, theme]);
|
||||
|
||||
// Delay the serialize process to the next event loop to avoid flickering
|
||||
// when copying code to the editor and the MDX preview shrinks.
|
||||
@ -94,7 +98,12 @@ export default function MdxPreview({
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<div className="rounded-lg border border-red-500/50 px-4 py-3 text-red-600">
|
||||
<p className="text-sm">
|
||||
<CircleAlert className="-mt-0.5 me-3 inline-flex opacity-60" size={16} strokeWidth={2} aria-hidden="true" />
|
||||
<CircleAlert
|
||||
className="-mt-0.5 me-3 inline-flex opacity-60"
|
||||
size={16}
|
||||
strokeWidth={2}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{error}
|
||||
</p>
|
||||
</div>
|
||||
@ -107,7 +116,12 @@ export default function MdxPreview({
|
||||
<div className="h-full flex items-center justify-center">
|
||||
<div className="rounded-lg border border-amber-500/50 px-4 py-3 text-amber-600">
|
||||
<p className="text-sm">
|
||||
<TriangleAlert className="-mt-0.5 me-3 inline-flex opacity-60" size={16} strokeWidth={2} aria-hidden="true" />
|
||||
<TriangleAlert
|
||||
className="-mt-0.5 me-3 inline-flex opacity-60"
|
||||
size={16}
|
||||
strokeWidth={2}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
No content to preview.
|
||||
</p>
|
||||
</div>
|
||||
@ -117,7 +131,7 @@ export default function MdxPreview({
|
||||
|
||||
return (
|
||||
<article className={cn("markdown-body", className)}>
|
||||
<MDXRemote {...mdxSource!} components={MdxComponents} />
|
||||
<MDXRemote {...mdxSource!} components={components} />
|
||||
</article>
|
||||
);
|
||||
}
|
||||
|
@ -1,171 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import dynamic from "next/dynamic";
|
||||
import { highlighter } from "@/lib/shiki";
|
||||
import type { editor } from "monaco-editor";
|
||||
import { Loading } from "@/components/loading";
|
||||
import { shikiToMonaco } from "@shikijs/monaco";
|
||||
import { useProblem } from "@/hooks/use-problem";
|
||||
import type { Monaco } from "@monaco-editor/react";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { connectToLanguageServer } from "@/lib/language-server";
|
||||
import type { MonacoLanguageClient } from "monaco-languageclient";
|
||||
import { DefaultEditorOptionConfig } from "@/config/editor-option";
|
||||
import { useEffect } from "react";
|
||||
import { CoreEditor } from "@/components/core-editor";
|
||||
import { useProblemEditorStore } from "@/stores/problem-editor";
|
||||
import type { LanguageServerConfig, Template } from "@/generated/client";
|
||||
|
||||
// Dynamically import Monaco Editor with SSR disabled
|
||||
const Editor = dynamic(
|
||||
async () => {
|
||||
await import("vscode");
|
||||
const monaco = await import("monaco-editor");
|
||||
interface ProblemEditorProps {
|
||||
problemId: string;
|
||||
templates: Template[];
|
||||
languageServerConfigs?: LanguageServerConfig[];
|
||||
}
|
||||
|
||||
self.MonacoEnvironment = {
|
||||
getWorker(_, label) {
|
||||
if (label === "json") {
|
||||
return new Worker(
|
||||
new URL("monaco-editor/esm/vs/language/json/json.worker.js", import.meta.url)
|
||||
);
|
||||
}
|
||||
if (label === "css" || label === "scss" || label === "less") {
|
||||
return new Worker(
|
||||
new URL("monaco-editor/esm/vs/language/css/css.worker.js", import.meta.url)
|
||||
);
|
||||
}
|
||||
if (label === "html" || label === "handlebars" || label === "razor") {
|
||||
return new Worker(
|
||||
new URL("monaco-editor/esm/vs/language/html/html.worker.js", import.meta.url)
|
||||
);
|
||||
}
|
||||
if (label === "typescript" || label === "javascript") {
|
||||
return new Worker(
|
||||
new URL("monaco-editor/esm/vs/language/typescript/ts.worker.js", import.meta.url)
|
||||
);
|
||||
}
|
||||
return new Worker(
|
||||
new URL("monaco-editor/esm/vs/editor/editor.worker.js", import.meta.url)
|
||||
);
|
||||
},
|
||||
};
|
||||
const { loader } = await import("@monaco-editor/react");
|
||||
loader.config({ monaco });
|
||||
return (await import("@monaco-editor/react")).Editor;
|
||||
},
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => <Loading />,
|
||||
}
|
||||
);
|
||||
|
||||
export function ProblemEditor() {
|
||||
export const ProblemEditor = ({
|
||||
problemId,
|
||||
templates,
|
||||
languageServerConfigs,
|
||||
}: ProblemEditorProps) => {
|
||||
const {
|
||||
hydrated,
|
||||
editor,
|
||||
language,
|
||||
value,
|
||||
path,
|
||||
setProblem,
|
||||
setValue,
|
||||
setEditor,
|
||||
setLspWebSocket,
|
||||
setMarkers,
|
||||
setWebSocket,
|
||||
currentLang,
|
||||
currentPath,
|
||||
currentTheme,
|
||||
currentValue,
|
||||
changeValue,
|
||||
currentEditorLanguageConfig,
|
||||
currentLanguageServerConfig,
|
||||
} = useProblem();
|
||||
} = useProblemEditorStore();
|
||||
|
||||
const monacoLanguageClientRef = useRef<MonacoLanguageClient | null>(null);
|
||||
|
||||
// Connect to LSP only if enabled
|
||||
const connectLSP = useCallback(async () => {
|
||||
if (!(currentLang && editor)) return;
|
||||
|
||||
// If there's an existing language client, stop it first
|
||||
if (monacoLanguageClientRef.current) {
|
||||
monacoLanguageClientRef.current.stop();
|
||||
monacoLanguageClientRef.current = null;
|
||||
setWebSocket(null);
|
||||
}
|
||||
|
||||
if (!currentEditorLanguageConfig || !currentLanguageServerConfig) return;
|
||||
|
||||
// Create a new language client
|
||||
try {
|
||||
const { client: monacoLanguageClient, webSocket } = await connectToLanguageServer(
|
||||
currentEditorLanguageConfig,
|
||||
currentLanguageServerConfig
|
||||
);
|
||||
monacoLanguageClientRef.current = monacoLanguageClient;
|
||||
setWebSocket(webSocket);
|
||||
} catch (error) {
|
||||
console.error("Failed to connect to LSP:", error);
|
||||
}
|
||||
}, [
|
||||
currentEditorLanguageConfig,
|
||||
currentLang,
|
||||
currentLanguageServerConfig,
|
||||
editor,
|
||||
setWebSocket,
|
||||
]);
|
||||
|
||||
// Reconnect to the LSP whenever language or lspConfig changes
|
||||
useEffect(() => {
|
||||
connectLSP();
|
||||
}, [connectLSP]);
|
||||
|
||||
// Cleanup the LSP connection when the component unmounts
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (monacoLanguageClientRef.current) {
|
||||
monacoLanguageClientRef.current.stop();
|
||||
monacoLanguageClientRef.current = null;
|
||||
setWebSocket(null);
|
||||
}
|
||||
};
|
||||
}, [setWebSocket]);
|
||||
|
||||
const handleEditorWillMount = useCallback((monaco: Monaco) => {
|
||||
shikiToMonaco(highlighter, monaco);
|
||||
}, []);
|
||||
|
||||
const handleOnMount = useCallback(
|
||||
async (editor: editor.IStandaloneCodeEditor) => {
|
||||
setEditor(editor);
|
||||
await connectLSP();
|
||||
},
|
||||
[setEditor, connectLSP]
|
||||
);
|
||||
|
||||
const handleEditorChange = useCallback(
|
||||
(value: string | undefined) => {
|
||||
if (value !== undefined) {
|
||||
changeValue(value);
|
||||
}
|
||||
},
|
||||
[changeValue]
|
||||
);
|
||||
|
||||
const handleEditorValidation = useCallback(
|
||||
(markers: editor.IMarker[]) => {
|
||||
setMarkers(markers);
|
||||
},
|
||||
[setMarkers]
|
||||
);
|
||||
|
||||
if (!hydrated) {
|
||||
return <Loading />;
|
||||
}
|
||||
setProblem(problemId, templates);
|
||||
}, [problemId, setProblem, templates]);
|
||||
|
||||
return (
|
||||
<Editor
|
||||
language={currentLang}
|
||||
theme={currentTheme}
|
||||
path={currentPath}
|
||||
value={currentValue}
|
||||
beforeMount={handleEditorWillMount}
|
||||
onMount={handleOnMount}
|
||||
onChange={handleEditorChange}
|
||||
onValidate={handleEditorValidation}
|
||||
options={DefaultEditorOptionConfig}
|
||||
loading={<Loading />}
|
||||
className="h-full w-full"
|
||||
<CoreEditor
|
||||
language={language}
|
||||
value={value}
|
||||
path={path}
|
||||
languageServerConfigs={languageServerConfigs}
|
||||
onEditorReady={setEditor}
|
||||
onLspWebSocketReady={setLspWebSocket}
|
||||
onMarkersReady={setMarkers}
|
||||
onChange={setValue}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -1,153 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Locale } from "@/config/i18n";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { enUS, zhCN } from "date-fns/locale";
|
||||
import { useProblem } from "@/hooks/use-problem";
|
||||
import { Clock4Icon, CpuIcon } from "lucide-react";
|
||||
import { useDockviewStore } from "@/stores/dockview";
|
||||
import { getStatusColorClass, statusMap } from "@/lib/status";
|
||||
import type { SubmissionWithTestcaseResult } from "@/types/prisma";
|
||||
import { EditorLanguageIcons } from "@/config/editor-language-icons";
|
||||
import { formatDistanceToNow, isBefore, subDays, format } from "date-fns";
|
||||
|
||||
interface SubmissionsTableProps {
|
||||
locale: Locale;
|
||||
submissions: SubmissionWithTestcaseResult[];
|
||||
}
|
||||
|
||||
const getLocale = (locale: Locale) => {
|
||||
switch (locale) {
|
||||
case "zh":
|
||||
return zhCN;
|
||||
case "en":
|
||||
default:
|
||||
return enUS;
|
||||
}
|
||||
}
|
||||
|
||||
export default function SubmissionsTable({ locale, submissions }: SubmissionsTableProps) {
|
||||
const s = useTranslations("StatusMessage");
|
||||
const t = useTranslations("SubmissionsTable");
|
||||
const { editorLanguageConfigs } = useProblem();
|
||||
const { api, setSubmission } = useDockviewStore();
|
||||
|
||||
const sortedSubmissions = [...submissions].sort(
|
||||
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
);
|
||||
|
||||
const handleRowClick = (submission: SubmissionWithTestcaseResult) => {
|
||||
if (!api) return;
|
||||
setSubmission(submission);
|
||||
|
||||
const panel = api.getPanel("Details");
|
||||
if (panel) {
|
||||
panel.api.setActive();
|
||||
} else {
|
||||
api.addPanel({
|
||||
id: "Details",
|
||||
component: "Details",
|
||||
tabComponent: "Details",
|
||||
title: s(`${submission.status}`),
|
||||
position: {
|
||||
referencePanel: "Submissions",
|
||||
direction: "within",
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<TableHeader className="bg-transparent">
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableHead className="w-[100px]">{t("Index")}</TableHead>
|
||||
<TableHead className="w-[170px]">{t("Status")}</TableHead>
|
||||
<TableHead className="w-[100px]">{t("Language")}</TableHead>
|
||||
<TableHead className="w-[100px]">{t("Time")}</TableHead>
|
||||
<TableHead className="w-[100px]">{t("Memory")}</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<tbody aria-hidden="true" className="table-row h-2" />
|
||||
|
||||
<TableBody className="[&_td:first-child]:rounded-l-lg [&_td:last-child]:rounded-r-lg">
|
||||
{sortedSubmissions.map((submission, index) => {
|
||||
const Icon = EditorLanguageIcons[submission.language];
|
||||
const createdAt = new Date(submission.createdAt);
|
||||
const localeInstance = getLocale(locale);
|
||||
const submittedDisplay = isBefore(createdAt, subDays(new Date(), 1))
|
||||
? format(createdAt, "yyyy-MM-dd")
|
||||
: formatDistanceToNow(createdAt, { addSuffix: true, locale: localeInstance });
|
||||
|
||||
const isEven = (submissions.length - index) % 2 === 0;
|
||||
const message = statusMap.get(submission.status)?.message;
|
||||
|
||||
return (
|
||||
<TableRow
|
||||
key={submission.id}
|
||||
onClick={() => handleRowClick(submission)}
|
||||
className={cn(
|
||||
"border-b-0 hover:text-blue-500 hover:bg-muted hover:cursor-pointer",
|
||||
isEven ? "" : "bg-muted/50"
|
||||
)}
|
||||
>
|
||||
<TableCell className="font-medium">
|
||||
{sortedSubmissions.length - index}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-col truncate">
|
||||
<span className={getStatusColorClass(submission.status)}>
|
||||
{s(`${message}`)}
|
||||
</span>
|
||||
<span className="text-xs">{submittedDisplay}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon size={16} aria-hidden="true" />
|
||||
<span className="truncate text-sm font-semibold mr-2">
|
||||
{
|
||||
editorLanguageConfigs.find(
|
||||
(config) => config.language === submission.language
|
||||
)?.label
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Clock4Icon size={16} aria-hidden="true" />
|
||||
<span>
|
||||
{submission.executionTime === null
|
||||
? "N/A"
|
||||
: `${submission.executionTime} ms`}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<CpuIcon size={16} aria-hidden="true" />
|
||||
<span>
|
||||
{submission.memoryUsage === null
|
||||
? "N/A"
|
||||
: `${submission.memoryUsage} MB`}
|
||||
</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
);
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/components/ui/tabs";
|
||||
import { TestcaseWithDetails } from "@/types/prisma";
|
||||
import TestcaseForm from "@/components/testcase-form";
|
||||
|
||||
interface TestcaseCardProps {
|
||||
testcases: TestcaseWithDetails;
|
||||
}
|
||||
|
||||
export default function TestcaseCard({ testcases }: TestcaseCardProps) {
|
||||
return (
|
||||
<Tabs defaultValue="case-1" className="items-center px-5 py-4">
|
||||
<TabsList className="bg-transparent p-0">
|
||||
{testcases.map((_, index) => (
|
||||
<TabsTrigger
|
||||
key={`tab-${index}`}
|
||||
value={`case-${index + 1}`}
|
||||
className="data-[state=active]:bg-muted data-[state=active]:shadow-none"
|
||||
>
|
||||
Case {index + 1}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
{testcases.map((testcase, index) => {
|
||||
const formData = testcase.data.reduce((acc, field) => {
|
||||
acc[field.label] = field.value;
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
|
||||
return (
|
||||
<TabsContent key={`content-${index}`} value={`case-${index + 1}`}>
|
||||
<TestcaseForm {...formData} />
|
||||
</TabsContent>
|
||||
);
|
||||
})}
|
||||
</Tabs>
|
||||
);
|
||||
}
|
@ -3,30 +3,11 @@
|
||||
import { useTheme } from "next-themes";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { MoonIcon, SunIcon } from "lucide-react";
|
||||
|
||||
function SunIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" {...props}>
|
||||
<path d="M12.5 10a2.5 2.5 0 1 1-5 0 2.5 2.5 0 0 1 5 0Z" />
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
d="M10 5.5v-1M13.182 6.818l.707-.707M14.5 10h1M13.182 13.182l.707.707M10 15.5v-1M6.11 13.889l.708-.707M4.5 10h1M6.11 6.111l.708.707"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function MoonIcon(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||
return (
|
||||
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" {...props}>
|
||||
<path d="M15.224 11.724a5.5 5.5 0 0 1-6.949-6.949 5.5 5.5 0 1 0 6.949 6.949Z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export function ThemeToggle() {
|
||||
export const ThemeToggle = () => {
|
||||
const { resolvedTheme, setTheme } = useTheme();
|
||||
const otherTheme = resolvedTheme === "dark" ? "light" : "dark";
|
||||
const alternateTheme = resolvedTheme === "dark" ? "light" : "dark";
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@ -37,12 +18,21 @@ export function ThemeToggle() {
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="flex h-6 w-6 items-center justify-center rounded-md transition"
|
||||
aria-label={mounted ? `Switch to ${otherTheme} theme` : "Toggle theme"}
|
||||
onClick={() => setTheme(otherTheme)}
|
||||
onClick={() => setTheme(alternateTheme)}
|
||||
aria-label={
|
||||
mounted ? `Switch to ${alternateTheme} theme` : "Toggle theme"
|
||||
}
|
||||
>
|
||||
<SunIcon className="h-5 w-5 stroke-foreground dark:hidden" />
|
||||
<MoonIcon className="hidden h-5 w-5 stroke-foreground dark:block" />
|
||||
<MoonIcon
|
||||
size={16}
|
||||
className="shrink-0 scale-0 opacity-0 transition-all dark:scale-100 dark:opacity-100"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<SunIcon
|
||||
size={16}
|
||||
className="absolute shrink-0 scale-100 opacity-100 transition-all dark:scale-0 dark:opacity-0"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
@ -1,31 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner } from "sonner"
|
||||
|
||||
type ToasterProps = React.ComponentProps<typeof Sonner>
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme()
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast:
|
||||
"group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg",
|
||||
description: "group-[.toast]:text-muted-foreground",
|
||||
actionButton:
|
||||
"group-[.toast]:bg-primary group-[.toast]:text-primary-foreground",
|
||||
cancelButton:
|
||||
"group-[.toast]:bg-muted group-[.toast]:text-muted-foreground",
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toaster }
|
12
src/config/difficulty.ts
Normal file
12
src/config/difficulty.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { Difficulty } from "@/generated/client";
|
||||
|
||||
export const getColorClassForDifficulty = (difficulty: Difficulty) => {
|
||||
switch (difficulty) {
|
||||
case Difficulty.EASY:
|
||||
return "text-green-500";
|
||||
case Difficulty.MEDIUM:
|
||||
return "text-yellow-500";
|
||||
case Difficulty.HARD:
|
||||
return "text-red-500";
|
||||
}
|
||||
};
|
@ -1,11 +0,0 @@
|
||||
import { EditorLanguage } from "@/generated/client";
|
||||
import { COriginal, CplusplusOriginal } from "devicons-react";
|
||||
|
||||
// Mapping between EditorLanguage and icons
|
||||
export const EditorLanguageIcons: Record<
|
||||
EditorLanguage,
|
||||
React.FunctionComponent<React.SVGProps<SVGElement> & { size?: number | string }>
|
||||
> = {
|
||||
[EditorLanguage.c]: COriginal,
|
||||
[EditorLanguage.cpp]: CplusplusOriginal,
|
||||
};
|
@ -1,3 +0,0 @@
|
||||
import { EditorLanguage } from "@/generated/client";
|
||||
|
||||
export const DEFAULT_EDITOR_LANGUAGE = EditorLanguage.c;
|
@ -1,3 +1,23 @@
|
||||
import { Language } from "@/generated/client";
|
||||
import COriginal from "devicons-react/icons/COriginal";
|
||||
import CplusplusOriginal from "devicons-react/icons/CplusplusOriginal";
|
||||
|
||||
export const LANGUAGES = Object.values(Language);
|
||||
|
||||
export const getIconForLanguage = (language: Language) => {
|
||||
switch (language) {
|
||||
case Language.c:
|
||||
return COriginal;
|
||||
case Language.cpp:
|
||||
return CplusplusOriginal;
|
||||
}
|
||||
};
|
||||
|
||||
export const getLabelForLanguage = (language: Language) => {
|
||||
switch (language) {
|
||||
case Language.c:
|
||||
return "C";
|
||||
case Language.cpp:
|
||||
return "C++";
|
||||
}
|
||||
};
|
||||
|
20
src/config/locale.ts
Normal file
20
src/config/locale.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { Locale } from "@/generated/client";
|
||||
import { enUS } from "date-fns/locale/en-US";
|
||||
import { zhCN } from "date-fns/locale/zh-CN";
|
||||
import { formatDistanceToNow, isBefore, subDays, format } from "date-fns";
|
||||
|
||||
export const getDateFunctionForLocale = (locale: Locale) => {
|
||||
switch (locale) {
|
||||
case Locale.en:
|
||||
return enUS;
|
||||
case Locale.zh:
|
||||
return zhCN;
|
||||
}
|
||||
};
|
||||
|
||||
export const formatSubmissionDate = (date: Date, locale: Locale) => {
|
||||
const localeInstance = getDateFunctionForLocale(locale);
|
||||
return isBefore(date, subDays(new Date(), 1))
|
||||
? format(date, "yyyy-MM-dd")
|
||||
: formatDistanceToNow(date, { addSuffix: true, locale: localeInstance });
|
||||
};
|
60
src/config/status.ts
Normal file
60
src/config/status.ts
Normal file
@ -0,0 +1,60 @@
|
||||
import { Status } from "@/generated/client";
|
||||
import { AlertTriangleIcon, BanIcon, CircleCheckIcon } from "lucide-react";
|
||||
|
||||
export const getIconForStatus = (status: Status) => {
|
||||
switch (status) {
|
||||
case Status.PD:
|
||||
case Status.QD:
|
||||
case Status.CP:
|
||||
case Status.CE:
|
||||
case Status.RU:
|
||||
case Status.TLE:
|
||||
case Status.MLE:
|
||||
case Status.RE:
|
||||
case Status.WA:
|
||||
return AlertTriangleIcon;
|
||||
case Status.CS:
|
||||
case Status.AC:
|
||||
return CircleCheckIcon;
|
||||
case Status.SE:
|
||||
return BanIcon;
|
||||
}
|
||||
};
|
||||
|
||||
export const getColorClassForStatus = (status: Status) => {
|
||||
switch (status) {
|
||||
case Status.PD:
|
||||
case Status.QD:
|
||||
return "text-gray-500";
|
||||
case Status.CP:
|
||||
case Status.CE:
|
||||
return "text-yellow-500";
|
||||
case Status.CS:
|
||||
case Status.AC:
|
||||
return "text-green-500";
|
||||
case Status.RU:
|
||||
case Status.TLE:
|
||||
return "text-blue-500";
|
||||
case Status.MLE:
|
||||
return "text-purple-500";
|
||||
case Status.RE:
|
||||
return "text-orange-500";
|
||||
case Status.WA:
|
||||
case Status.SE:
|
||||
return "text-red-500";
|
||||
}
|
||||
};
|
||||
|
||||
export const getColorClassForLspStatus = (webSocket: WebSocket | null) => {
|
||||
if (!webSocket) return "bg-gray-500";
|
||||
switch (webSocket.readyState) {
|
||||
case WebSocket.CONNECTING:
|
||||
return "bg-sky-500 animate-pulse";
|
||||
case WebSocket.OPEN:
|
||||
return "bg-emerald-500";
|
||||
case WebSocket.CLOSING:
|
||||
return "bg-amber-500 animate-pulse";
|
||||
case WebSocket.CLOSED:
|
||||
return "bg-red-500";
|
||||
}
|
||||
};
|
81
src/features/problems/bot/components/content.tsx
Normal file
81
src/features/problems/bot/components/content.tsx
Normal file
@ -0,0 +1,81 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import { getLocale } from "next-intl/server";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Locale, ProblemLocalization } from "@/generated/client";
|
||||
import { BotForm } from "@/features/problems/bot/components/form";
|
||||
|
||||
const getLocalizedDescription = (
|
||||
localizations: ProblemLocalization[],
|
||||
locale: Locale
|
||||
) => {
|
||||
if (!localizations || localizations.length === 0) {
|
||||
return "Unknown Description";
|
||||
}
|
||||
|
||||
const localization = localizations.find(
|
||||
(localization) => localization.locale === locale
|
||||
);
|
||||
|
||||
return (
|
||||
localization?.content ?? localizations[0].content ?? "Unknown Description"
|
||||
);
|
||||
};
|
||||
|
||||
interface BotContentProps {
|
||||
problemId: string;
|
||||
}
|
||||
|
||||
export const BotContent = async ({ problemId }: BotContentProps) => {
|
||||
const locale = await getLocale();
|
||||
|
||||
const descriptions = await prisma.problemLocalization.findMany({
|
||||
where: {
|
||||
problemId,
|
||||
type: "DESCRIPTION",
|
||||
},
|
||||
});
|
||||
|
||||
const description = getLocalizedDescription(descriptions, locale as Locale);
|
||||
|
||||
return (
|
||||
<div className="relative flex-1">
|
||||
<div className="absolute h-full w-full">
|
||||
<BotForm description={description} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const BotContentSkeleton = () => {
|
||||
return (
|
||||
<div className="relative h-full w-full">
|
||||
<div className="absolute h-full w-full p-4 md:p-6">
|
||||
{/* Title skeleton */}
|
||||
<Skeleton className="mb-6 h-8 w-3/4" />
|
||||
|
||||
{/* Content skeletons */}
|
||||
<Skeleton className="mb-4 h-4 w-full" />
|
||||
<Skeleton className="mb-4 h-4 w-5/6" />
|
||||
<Skeleton className="mb-4 h-4 w-2/3" />
|
||||
<Skeleton className="mb-4 h-4 w-full" />
|
||||
<Skeleton className="mb-4 h-4 w-4/5" />
|
||||
|
||||
{/* Example section heading */}
|
||||
<Skeleton className="mb-4 mt-8 h-6 w-1/4" />
|
||||
|
||||
{/* Example content */}
|
||||
<Skeleton className="mb-4 h-4 w-full" />
|
||||
<Skeleton className="mb-4 h-4 w-5/6" />
|
||||
|
||||
{/* Code block skeleton */}
|
||||
<div className="mb-6">
|
||||
<Skeleton className="h-40 w-full rounded-md" />
|
||||
</div>
|
||||
|
||||
{/* More content */}
|
||||
<Skeleton className="mb-4 h-4 w-full" />
|
||||
<Skeleton className="mb-4 h-4 w-3/4" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
133
src/features/problems/bot/components/form.tsx
Normal file
133
src/features/problems/bot/components/form.tsx
Normal file
@ -0,0 +1,133 @@
|
||||
"use client";
|
||||
|
||||
import { toast } from "sonner";
|
||||
import { useCallback } from "react";
|
||||
import { useChat } from "@ai-sdk/react";
|
||||
import {
|
||||
ChatBubble,
|
||||
ChatBubbleMessage,
|
||||
} from "@/components/ui/chat/chat-bubble";
|
||||
import { useTranslations } from "next-intl";
|
||||
import MdxPreview from "@/components/mdx-preview";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { BotIcon, SendHorizonal } from "lucide-react";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { PreDetail } from "@/components/content/pre-detail";
|
||||
import { TooltipButton } from "@/components/tooltip-button";
|
||||
import { useProblemEditorStore } from "@/stores/problem-editor";
|
||||
import { MdxComponents } from "@/components/content/mdx-components";
|
||||
import { ChatMessageList } from "@/components/ui/chat/chat-message-list";
|
||||
|
||||
interface BotFormProps {
|
||||
description: string;
|
||||
}
|
||||
|
||||
export const BotForm = ({ description }: BotFormProps) => {
|
||||
const t = useTranslations("Bot");
|
||||
const { problem, language, value } = useProblemEditorStore();
|
||||
|
||||
const { messages, input, handleInputChange, setMessages, handleSubmit } =
|
||||
useChat({
|
||||
initialMessages: [
|
||||
{
|
||||
id: problem?.problemId || "",
|
||||
role: "system",
|
||||
content: `Problem description:\n${description}`,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const handleFormSubmit = useCallback(
|
||||
(e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (!input.trim()) {
|
||||
toast.error("Input cannot be empty");
|
||||
return;
|
||||
}
|
||||
|
||||
const currentCodeMessage = {
|
||||
id: problem?.problemId || "",
|
||||
role: "system" as const,
|
||||
content: `Current code:\n\`\`\`${language}\n${value}\n\`\`\``,
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, currentCodeMessage]);
|
||||
handleSubmit();
|
||||
},
|
||||
[handleSubmit, input, language, problem?.problemId, setMessages, value]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
{messages.some(
|
||||
(message) => message.role === "user" || message.role === "assistant"
|
||||
) ? (
|
||||
<div className="relative flex-1">
|
||||
<div className="absolute h-full w-full">
|
||||
<ScrollArea className="h-full [&>[data-radix-scroll-area-viewport]>div:min-w-0 [&>[data-radix-scroll-area-viewport]>div]:!block">
|
||||
<ChatMessageList>
|
||||
{messages
|
||||
.filter(
|
||||
(message) =>
|
||||
message.role === "user" || message.role === "assistant"
|
||||
)
|
||||
.map((message) => (
|
||||
<ChatBubble
|
||||
key={message.id}
|
||||
layout="ai"
|
||||
className="border-b pb-4"
|
||||
>
|
||||
<ChatBubbleMessage layout="ai">
|
||||
<MdxPreview
|
||||
source={message.content}
|
||||
components={{ ...MdxComponents, pre: PreDetail }}
|
||||
/>
|
||||
</ChatBubbleMessage>
|
||||
</ChatBubble>
|
||||
))}
|
||||
</ChatMessageList>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-full flex flex-col items-center justify-center gap-2 text-muted-foreground">
|
||||
<BotIcon />
|
||||
<span>{t("title")}</span>
|
||||
<span className="font-thin text-xs">{t("description")}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<footer className="h-36 flex flex-none">
|
||||
<form onSubmit={handleFormSubmit} className="w-full p-4 pt-0 relative">
|
||||
<Textarea
|
||||
value={input}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={(e) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
if (input.trim()) {
|
||||
handleFormSubmit(e);
|
||||
} else {
|
||||
toast.error("Input cannot be empty");
|
||||
}
|
||||
}
|
||||
}}
|
||||
className="h-full bg-muted border-transparent shadow-none rounded-lg"
|
||||
placeholder={t("placeholder")}
|
||||
/>
|
||||
|
||||
<TooltipButton
|
||||
tooltipContent="Ctrl + Enter"
|
||||
type="submit"
|
||||
variant="ghost"
|
||||
className="absolute bottom-6 right-6 h-6 w-auto px-2"
|
||||
aria-label="Send Message"
|
||||
>
|
||||
<SendHorizonal className="size-4" />
|
||||
</TooltipButton>
|
||||
</form>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
};
|
19
src/features/problems/bot/components/panel.tsx
Normal file
19
src/features/problems/bot/components/panel.tsx
Normal file
@ -0,0 +1,19 @@
|
||||
import { Suspense } from "react";
|
||||
import {
|
||||
BotContent,
|
||||
BotContentSkeleton,
|
||||
} from "@/features/problems/bot/components/content";
|
||||
|
||||
interface BotPanelProps {
|
||||
problemId: string;
|
||||
}
|
||||
|
||||
export const BotPanel = ({ problemId }: BotPanelProps) => {
|
||||
return (
|
||||
<div className="h-full flex flex-col border border-t-0 border-muted rounded-b-lg bg-background overflow-hidden">
|
||||
<Suspense fallback={<BotContentSkeleton />}>
|
||||
<BotContent problemId={problemId} />
|
||||
</Suspense>
|
||||
</div>
|
||||
);
|
||||
};
|
90
src/features/problems/bot/components/view-bot-button.tsx
Normal file
90
src/features/problems/bot/components/view-bot-button.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { BotIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Toggle } from "@/components/ui/toggle";
|
||||
import { Actions, DockLocation } from "flexlayout-react";
|
||||
import { useProblemFlexLayoutStore } from "@/stores/problem-flexlayout";
|
||||
|
||||
export const ViewBotButton = () => {
|
||||
const t = useTranslations();
|
||||
const { model } = useProblemFlexLayoutStore();
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
const [isBotVisible, setBotVisible] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (model) {
|
||||
const botTab = model.getNodeById("bot");
|
||||
setBotVisible(!!botTab);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [model]);
|
||||
|
||||
const handleBotToggle = (newState: boolean) => {
|
||||
if (!model) return;
|
||||
|
||||
const botTab = model.getNodeById("bot");
|
||||
|
||||
if (newState) {
|
||||
if (botTab) {
|
||||
model.doAction(Actions.selectTab("bot"));
|
||||
} else {
|
||||
model.doAction(
|
||||
Actions.addNode(
|
||||
{
|
||||
type: "tab",
|
||||
id: "bot",
|
||||
name: "Bot",
|
||||
component: "bot",
|
||||
enableClose: false,
|
||||
},
|
||||
model.getRoot().getId(),
|
||||
DockLocation.RIGHT,
|
||||
-1
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (botTab) {
|
||||
model.doAction(Actions.deleteTab("bot"));
|
||||
}
|
||||
}
|
||||
|
||||
setBotVisible(newState);
|
||||
};
|
||||
|
||||
return (
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div>
|
||||
<Toggle
|
||||
aria-label="Toggle Bot"
|
||||
pressed={isBotVisible}
|
||||
onPressedChange={handleBotToggle}
|
||||
size="sm"
|
||||
className="rounded-lg"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<BotIcon size={16} aria-hidden="true" />
|
||||
</Toggle>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent className="px-2 py-1 text-xs">
|
||||
<p>
|
||||
{isBotVisible
|
||||
? t("BotVisibilityToggle.close")
|
||||
: t("BotVisibilityToggle.open")}
|
||||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
@ -12,8 +12,15 @@ export const CodeContent = async ({ problemId }: CodeContentProps) => {
|
||||
problemId,
|
||||
},
|
||||
});
|
||||
const languageServerConfigs = await prisma.languageServerConfig.findMany();
|
||||
|
||||
return <ProblemEditor problemId={problemId} templates={templates} />;
|
||||
return (
|
||||
<ProblemEditor
|
||||
problemId={problemId}
|
||||
templates={templates}
|
||||
languageServerConfigs={languageServerConfigs}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const CodeContentSkeleton = () => {
|
||||
|
@ -1,14 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useProblem } from "@/hooks/use-problem";
|
||||
import { useEffect, useState } from "react";
|
||||
import { CircleXIcon, TriangleAlertIcon } from "lucide-react";
|
||||
|
||||
interface WorkspaceEditorFooterProps {
|
||||
className?: string;
|
||||
}
|
||||
import { useProblemEditorStore } from "@/stores/problem-editor";
|
||||
|
||||
let MarkerSeverity: typeof import("monaco-editor").MarkerSeverity;
|
||||
if (typeof window !== "undefined") {
|
||||
@ -17,13 +13,17 @@ if (typeof window !== "undefined") {
|
||||
});
|
||||
}
|
||||
|
||||
export function WorkspaceEditorFooter({
|
||||
className,
|
||||
...props
|
||||
}: WorkspaceEditorFooterProps) {
|
||||
interface CodeFooterProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const CodeFooter = ({ className }: CodeFooterProps) => {
|
||||
const t = useTranslations("WorkspaceEditorFooter");
|
||||
const { editor, markers } = useProblem();
|
||||
const [position, setPosition] = useState<{ lineNumber: number; column: number } | null>(null);
|
||||
const { editor, markers } = useProblemEditorStore();
|
||||
const [position, setPosition] = useState<{
|
||||
lineNumber: number;
|
||||
column: number;
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return;
|
||||
@ -48,26 +48,41 @@ export function WorkspaceEditorFooter({
|
||||
|
||||
return (
|
||||
<footer
|
||||
{...props}
|
||||
className={cn("h-9 flex flex-none items-center px-3 py-2 bg-muted", className)}
|
||||
className={cn(
|
||||
"h-9 flex flex-none items-center px-3 py-2 bg-muted",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="w-full flex items-center justify-between px-0.5 truncate">
|
||||
<div className="flex gap-3">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<CircleXIcon className="text-red-500" size={20} aria-hidden="true" />
|
||||
<CircleXIcon
|
||||
className="text-red-500"
|
||||
size={20}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{markers.filter((m) => m.severity === MarkerSeverity.Error).length}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<TriangleAlertIcon className="text-yellow-500" size={20} aria-hidden="true" />
|
||||
{markers.filter((m) => m.severity === MarkerSeverity.Warning).length}
|
||||
<TriangleAlertIcon
|
||||
className="text-yellow-500"
|
||||
size={20}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
{
|
||||
markers.filter((m) => m.severity === MarkerSeverity.Warning)
|
||||
.length
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<span className="truncate">
|
||||
{position
|
||||
? `${t("Row")} ${position.lineNumber}, ${t("Column")} ${position.column}`
|
||||
? `${t("Row")} ${position.lineNumber}, ${t("Column")} ${
|
||||
position.column
|
||||
}`
|
||||
: `${t("Row")} -, ${t("Column")} -`}
|
||||
</span>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
};
|
@ -3,6 +3,7 @@ import {
|
||||
CodeContent,
|
||||
CodeContentSkeleton,
|
||||
} from "@/features/problems/code/components/content";
|
||||
import { CodeFooter } from "@/features/problems/code/components/footer";
|
||||
import { CodeToolbar } from "@/features/problems/code/components/toolbar/code-toolbar";
|
||||
|
||||
interface CodePanelProps {
|
||||
@ -11,7 +12,7 @@ interface CodePanelProps {
|
||||
|
||||
export const CodePanel = ({ problemId }: CodePanelProps) => {
|
||||
return (
|
||||
<div className="h-full flex flex-col border border-t-0 border-muted rounded-b-3xl bg-background overflow-hidden">
|
||||
<div className="h-full flex flex-col border border-t-0 border-muted rounded-b-lg bg-background overflow-hidden">
|
||||
<CodeToolbar className="border-b" />
|
||||
<div className="relative flex-1">
|
||||
<div className="absolute h-full w-full">
|
||||
@ -20,6 +21,7 @@ export const CodePanel = ({ problemId }: CodePanelProps) => {
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
<CodeFooter />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -0,0 +1,91 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { analyzeComplexity } from "@/app/actions/analyze";
|
||||
import { TooltipButton } from "@/components/tooltip-button";
|
||||
import { ChartBarIcon, LoaderCircleIcon } from "lucide-react";
|
||||
import { AnalyzeComplexityResponse } from "@/types/complexity";
|
||||
import { useProblemEditorStore } from "@/stores/problem-editor";
|
||||
import { useProblemEditorActions } from "@/features/problems/code/hooks/use-problem-editor-actions";
|
||||
|
||||
export const AnalyzeButton = () => {
|
||||
const t = useTranslations("WorkspaceEditorHeader.AnalyzeButton");
|
||||
const { value } = useProblemEditorStore();
|
||||
const { canExecute } = useProblemEditorActions();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [complexity, setComplexity] =
|
||||
useState<AnalyzeComplexityResponse | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const handleClick = async () => {
|
||||
setError(null);
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const complexity = await analyzeComplexity(value);
|
||||
setComplexity(complexity);
|
||||
setOpen(true);
|
||||
} catch (error) {
|
||||
console.error("Error analyzing complexity:", error);
|
||||
setComplexity(null);
|
||||
setError(t("Error"));
|
||||
setOpen(true);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TooltipButton
|
||||
tooltipContent={t("TooltipContent")}
|
||||
onClick={handleClick}
|
||||
disabled={!canExecute || isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<LoaderCircleIcon
|
||||
className="opacity-60 animate-spin"
|
||||
size={16}
|
||||
strokeWidth={2}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<ChartBarIcon size={16} strokeWidth={2} aria-hidden="true" />
|
||||
)}
|
||||
</TooltipButton>
|
||||
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center justify-center pb-1.5">
|
||||
{t("ComplexityAnalysis")}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="flex items-center justify-center pt-2">
|
||||
{error ? (
|
||||
<span className="text-red-500">{error}</span>
|
||||
) : (
|
||||
<span className="h-full flex flex-col gap-2">
|
||||
<span>
|
||||
{t("TimeComplexity")} <strong>{complexity?.time}</strong>
|
||||
</span>
|
||||
<span>
|
||||
{t("SpaceComplexity")} <strong>{complexity?.space}</strong>
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
@ -7,6 +7,8 @@ import {
|
||||
ResetButton,
|
||||
UndoButton,
|
||||
} from "@/features/problems/code/components/toolbar";
|
||||
import { AnalyzeButton } from "./actions/analyze-button";
|
||||
import { LspConnectionIndicator } from "./controls/lsp-connection-indicator";
|
||||
|
||||
interface CodeToolbarProps {
|
||||
className?: string;
|
||||
@ -20,8 +22,10 @@ export const CodeToolbar = async ({ className }: CodeToolbarProps) => {
|
||||
<div className="absolute flex w-full items-center justify-between px-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<LanguageSelector />
|
||||
<LspConnectionIndicator />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<AnalyzeButton />
|
||||
<ResetButton />
|
||||
<UndoButton />
|
||||
<RedoButton />
|
||||
|
@ -1,5 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
getIconForLanguage,
|
||||
getLabelForLanguage,
|
||||
LANGUAGES,
|
||||
} from "@/config/language";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@ -7,29 +12,7 @@ import {
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Language } from "@/generated/client";
|
||||
import { LANGUAGES } from "@/config/language";
|
||||
import COriginal from "devicons-react/icons/COriginal";
|
||||
import { useProblemEditorStore } from "@/stores/problem-editor";
|
||||
import CplusplusOriginal from "devicons-react/icons/CplusplusOriginal";
|
||||
|
||||
const getIconForLanguage = (language: Language) => {
|
||||
switch (language) {
|
||||
case Language.c:
|
||||
return <COriginal size={16} aria-hidden="true" />;
|
||||
case Language.cpp:
|
||||
return <CplusplusOriginal size={16} aria-hidden="true" />;
|
||||
}
|
||||
};
|
||||
|
||||
const getLabelForLanguage = (language: Language) => {
|
||||
switch (language) {
|
||||
case Language.c:
|
||||
return "C";
|
||||
case Language.cpp:
|
||||
return "C++";
|
||||
}
|
||||
};
|
||||
|
||||
export const LanguageSelector = () => {
|
||||
const { language, setLanguage } = useProblemEditorStore();
|
||||
@ -40,14 +23,19 @@ export const LanguageSelector = () => {
|
||||
<SelectValue placeholder="Select language" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="[&_*[role=option]>span>svg]:shrink-0 [&_*[role=option]>span>svg]:text-muted-foreground/80 [&_*[role=option]>span]:end-2 [&_*[role=option]>span]:start-auto [&_*[role=option]>span]:flex [&_*[role=option]>span]:items-center [&_*[role=option]>span]:gap-2 [&_*[role=option]]:pe-8 [&_*[role=option]]:ps-2">
|
||||
{LANGUAGES.map((language) => (
|
||||
<SelectItem key={language} value={language}>
|
||||
{getIconForLanguage(language)}
|
||||
<span className="truncate text-sm font-semibold mr-2">
|
||||
{getLabelForLanguage(language)}
|
||||
</span>
|
||||
</SelectItem>
|
||||
))}
|
||||
{LANGUAGES.map((language) => {
|
||||
const Icon = getIconForLanguage(language);
|
||||
const label = getLabelForLanguage(language);
|
||||
|
||||
return (
|
||||
<SelectItem key={language} value={language}>
|
||||
<Icon size={16} aria-hidden="true" />
|
||||
<span className="truncate text-sm font-semibold mr-2">
|
||||
{label}
|
||||
</span>
|
||||
</SelectItem>
|
||||
);
|
||||
})}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
);
|
||||
|
@ -0,0 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { getColorClassForLspStatus } from "@/config/status";
|
||||
import { useProblemEditorStore } from "@/stores/problem-editor";
|
||||
|
||||
export const LspConnectionIndicator = () => {
|
||||
const { lspWebSocket } = useProblemEditorStore();
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-6 w-auto px-2 py-0 gap-1 border-none shadow-none hover:bg-muted"
|
||||
>
|
||||
<div className="h-3 w-3 flex items-center justify-center">
|
||||
<div
|
||||
className={cn(
|
||||
"h-1.5 w-1.5 rounded-full transition-all",
|
||||
getColorClassForLspStatus(lspWebSocket)
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<span>LSP</span>
|
||||
</Button>
|
||||
);
|
||||
};
|
@ -1,33 +1,33 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Suspense } from "react";
|
||||
import { BackButton } from "@/components/back-button";
|
||||
import { JudgeButton } from "@/features/problems/components/judge-button";
|
||||
import { UserAvatar, UserAvatarSkeleton } from "@/components/user-avatar";
|
||||
import { JudgeCodeButton } from "@/features/problems/components/judge-code-button";
|
||||
import { NavigateBackButton } from "@/features/problems/components/navigate-back-button";
|
||||
import { ViewBotButton } from "@/features/problems/bot/components/view-bot-button";
|
||||
|
||||
interface ProblemHeaderProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const ProblemHeader = ({ className }: ProblemHeaderProps) => {
|
||||
export const ProblemHeader = ({ className }: ProblemHeaderProps) => {
|
||||
return (
|
||||
<header
|
||||
className={cn("relative flex h-12 flex-none items-center", className)}
|
||||
>
|
||||
<div className="container mx-auto flex h-full items-center justify-between px-4">
|
||||
<div className="flex items-center">
|
||||
<NavigateBackButton href="/problemset" />
|
||||
<BackButton href="/problemset" />
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center gap-4">
|
||||
<ViewBotButton />
|
||||
<Suspense fallback={<UserAvatarSkeleton />}>
|
||||
<UserAvatar />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
<div className="absolute inset-y-0 left-1/2 z-10 flex -translate-x-1/2 items-center">
|
||||
<JudgeCodeButton />
|
||||
<JudgeButton />
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export { ProblemHeader };
|
@ -3,20 +3,21 @@
|
||||
import { toast } from "sonner";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useState } from "react";
|
||||
import { Actions } from "flexlayout-react";
|
||||
import { judge } from "@/app/actions/judge";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { LoaderCircleIcon, PlayIcon } from "lucide-react";
|
||||
import { TooltipButton } from "@/components/tooltip-button";
|
||||
import { useProblemEditorStore } from "@/stores/problem-editor";
|
||||
import { useProblemDockviewStore } from "@/stores/problem-dockview";
|
||||
import { JudgeToast } from "@/features/problems/components/judge-toast";
|
||||
import { useProblemFlexLayoutStore } from "@/stores/problem-flexlayout";
|
||||
|
||||
interface JudgeButtonProps {
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export const JudgeButton = ({ className }: JudgeButtonProps) => {
|
||||
const { api } = useProblemDockviewStore();
|
||||
const { model } = useProblemFlexLayoutStore();
|
||||
const { problem, language, value } = useProblemEditorStore();
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
const t = useTranslations("PlaygroundHeader.RunCodeButton");
|
||||
@ -28,10 +29,10 @@ export const JudgeButton = ({ className }: JudgeButtonProps) => {
|
||||
const status = await judge(problem.problemId, language, value);
|
||||
toast.custom((t) => <JudgeToast t={t} status={status} />);
|
||||
|
||||
const panel = api?.getPanel("submission");
|
||||
if (panel && !panel.api.isActive) {
|
||||
panel.api.setActive();
|
||||
if (model) {
|
||||
model.doAction(Actions.selectTab("submission"));
|
||||
}
|
||||
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
@ -44,9 +45,7 @@ export const JudgeButton = ({ className }: JudgeButtonProps) => {
|
||||
className
|
||||
)}
|
||||
onClick={handleJudge}
|
||||
disabled={
|
||||
isLoading || !problem?.problemId || !api?.getPanel("submission")
|
||||
}
|
||||
disabled={isLoading || !problem?.problemId || !model}
|
||||
>
|
||||
{isLoading ? (
|
||||
<LoaderCircleIcon
|
||||
|
@ -1,64 +1,16 @@
|
||||
import {
|
||||
AlertTriangleIcon,
|
||||
BanIcon,
|
||||
CircleCheckIcon,
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { XIcon } from "lucide-react";
|
||||
import { Status } from "@/generated/client";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { getColorClassForStatus, getIconForStatus } from "@/config/status";
|
||||
|
||||
interface JudgeToastProps {
|
||||
t: number | string;
|
||||
status: Status;
|
||||
}
|
||||
|
||||
const getIconForStatus = (status: Status) => {
|
||||
switch (status) {
|
||||
case Status.PD:
|
||||
case Status.QD:
|
||||
case Status.CP:
|
||||
case Status.CE:
|
||||
case Status.RU:
|
||||
case Status.TLE:
|
||||
case Status.MLE:
|
||||
case Status.RE:
|
||||
case Status.WA:
|
||||
return AlertTriangleIcon;
|
||||
case Status.CS:
|
||||
case Status.AC:
|
||||
return CircleCheckIcon;
|
||||
case Status.SE:
|
||||
return BanIcon;
|
||||
}
|
||||
};
|
||||
|
||||
const getColorClassForStatus = (status: Status) => {
|
||||
switch (status) {
|
||||
case Status.PD:
|
||||
case Status.QD:
|
||||
return "text-gray-500";
|
||||
case Status.CP:
|
||||
case Status.CE:
|
||||
return "text-yellow-500";
|
||||
case Status.CS:
|
||||
case Status.AC:
|
||||
return "text-green-500";
|
||||
case Status.RU:
|
||||
case Status.TLE:
|
||||
return "text-blue-500";
|
||||
case Status.MLE:
|
||||
return "text-purple-500";
|
||||
case Status.RE:
|
||||
return "text-orange-500";
|
||||
case Status.WA:
|
||||
case Status.SE:
|
||||
return "text-red-500";
|
||||
}
|
||||
};
|
||||
|
||||
export const JudgeToast = ({ t, status }: JudgeToastProps) => {
|
||||
const s = useTranslations("StatusMessage");
|
||||
const Icon = getIconForStatus(status);
|
||||
|
@ -1,34 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useLocale } from "next-intl";
|
||||
import type { AddPanelOptions } from "dockview";
|
||||
import { Dockview, type PanelParams } from "@/components/dockview";
|
||||
import { useProblemDockviewStore } from "@/stores/problem-dockview";
|
||||
|
||||
interface ProblemDockviewProps {
|
||||
components: Record<string, React.ReactNode>;
|
||||
tabComponents: Record<string, React.ReactNode>;
|
||||
panelOptions: AddPanelOptions<PanelParams>[];
|
||||
}
|
||||
|
||||
const ProblemDockview = ({
|
||||
components,
|
||||
tabComponents,
|
||||
panelOptions,
|
||||
}: ProblemDockviewProps) => {
|
||||
const locale = useLocale();
|
||||
const { setApi } = useProblemDockviewStore();
|
||||
|
||||
return (
|
||||
<Dockview
|
||||
key={locale}
|
||||
storageKey="dockview:problem"
|
||||
onApiReady={setApi}
|
||||
components={components}
|
||||
tabComponents={tabComponents}
|
||||
panelOptions={panelOptions}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export { ProblemDockview };
|
94
src/features/problems/components/problem-flexlayout.tsx
Normal file
94
src/features/problems/components/problem-flexlayout.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
BotIcon,
|
||||
CircleCheckBigIcon,
|
||||
FileTextIcon,
|
||||
FlaskConicalIcon,
|
||||
SquareCheckIcon,
|
||||
SquarePenIcon,
|
||||
} from "lucide-react";
|
||||
import "@/styles/flexlayout.css";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useProblemFlexLayoutStore } from "@/stores/problem-flexlayout";
|
||||
import { ITabRenderValues, Layout, Model, TabNode } from "flexlayout-react";
|
||||
|
||||
interface ProblemFlexLayoutProps {
|
||||
components: Record<string, React.ReactNode>;
|
||||
}
|
||||
|
||||
export const ProblemFlexLayout = ({ components }: ProblemFlexLayoutProps) => {
|
||||
const t = useTranslations("ProblemPage");
|
||||
const { model, setModel, jsonModel, setJsonModel } =
|
||||
useProblemFlexLayoutStore();
|
||||
|
||||
useEffect(() => {
|
||||
if (!model) {
|
||||
const model = Model.fromJson(jsonModel);
|
||||
setModel(model);
|
||||
}
|
||||
}, [jsonModel, model, setModel]);
|
||||
|
||||
const onModelChange = useCallback(
|
||||
(model: Model) => {
|
||||
const jsonModel = model.toJson();
|
||||
setJsonModel(jsonModel);
|
||||
},
|
||||
[setJsonModel]
|
||||
);
|
||||
|
||||
const factory = useCallback(
|
||||
(node: TabNode) => {
|
||||
const component = node.getComponent();
|
||||
return component ? components[component] : null;
|
||||
},
|
||||
[components]
|
||||
);
|
||||
|
||||
const onRenderTab = useCallback(
|
||||
(node: TabNode, renderValues: ITabRenderValues) => {
|
||||
const Icon = getIconForTab(node.getId());
|
||||
renderValues.leading = Icon ? (
|
||||
<Icon className="opacity-60" size={16} aria-hidden="true" />
|
||||
) : null;
|
||||
renderValues.content = (
|
||||
<span className="text-sm font-medium">
|
||||
{t(node.getName()) || node.getName()}
|
||||
</span>
|
||||
);
|
||||
},
|
||||
[t]
|
||||
);
|
||||
|
||||
if (!model) return null;
|
||||
|
||||
return (
|
||||
<Layout
|
||||
model={model}
|
||||
factory={factory}
|
||||
onRenderTab={onRenderTab}
|
||||
onModelChange={onModelChange}
|
||||
realtimeResize={true}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const getIconForTab = (id: string) => {
|
||||
switch (id) {
|
||||
case "description":
|
||||
return FileTextIcon;
|
||||
case "solution":
|
||||
return FlaskConicalIcon;
|
||||
case "submission":
|
||||
return CircleCheckBigIcon;
|
||||
case "detail":
|
||||
return CircleCheckBigIcon;
|
||||
case "code":
|
||||
return SquarePenIcon;
|
||||
case "testcase":
|
||||
return SquareCheckIcon;
|
||||
case "bot":
|
||||
return BotIcon;
|
||||
}
|
||||
};
|
@ -10,7 +10,7 @@ interface DescriptionPanelProps {
|
||||
|
||||
export const DescriptionPanel = ({ problemId }: DescriptionPanelProps) => {
|
||||
return (
|
||||
<div className="h-full flex flex-col border border-t-0 border-muted rounded-b-3xl bg-background overflow-hidden">
|
||||
<div className="h-full flex flex-col border border-t-0 border-muted rounded-b-lg bg-background overflow-hidden">
|
||||
<div className="relative flex-1">
|
||||
<div className="absolute h-full w-full">
|
||||
<Suspense fallback={<DescriptionContentSkeleton />}>
|
||||
|
87
src/features/problems/detail/components/analyze-button.tsx
Normal file
87
src/features/problems/detail/components/analyze-button.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/components/ui/dialog";
|
||||
import { useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { analyzeComplexity } from "@/app/actions/analyze";
|
||||
import { AnalyzeComplexityResponse } from "@/types/complexity";
|
||||
import { LoaderCircleIcon, WandSparklesIcon } from "lucide-react";
|
||||
|
||||
interface AnalyzeButtonProps {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export const AnalyzeButton = ({ value }: AnalyzeButtonProps) => {
|
||||
const t = useTranslations("WorkspaceEditorHeader.AnalyzeButton");
|
||||
const [open, setOpen] = useState(false);
|
||||
const [complexity, setComplexity] =
|
||||
useState<AnalyzeComplexityResponse | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
const handleClick = async () => {
|
||||
setError(null);
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const complexity = await analyzeComplexity(value);
|
||||
setComplexity(complexity);
|
||||
setOpen(true);
|
||||
} catch (error) {
|
||||
console.error("Error analyzing complexity:", error);
|
||||
setComplexity(null);
|
||||
setError(t("Error"));
|
||||
setOpen(true);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button variant="ghost" disabled={isLoading} onClick={handleClick}>
|
||||
{isLoading ? (
|
||||
<LoaderCircleIcon
|
||||
className="opacity-60 animate-spin"
|
||||
size={16}
|
||||
strokeWidth={2}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
) : (
|
||||
<WandSparklesIcon size={16} aria-hidden="true" />
|
||||
)}
|
||||
<Label>{isLoading ? t("Analyzing") : t("ComplexityAnalysis")}</Label>
|
||||
</Button>
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center justify-center pb-1.5">
|
||||
{t("ComplexityAnalysis")}
|
||||
</DialogTitle>
|
||||
<DialogDescription className="flex items-center justify-center pt-2">
|
||||
{error ? (
|
||||
<span className="text-red-500">{error}</span>
|
||||
) : (
|
||||
<span className="h-full flex flex-col gap-2">
|
||||
<span>
|
||||
{t("TimeComplexity")} <strong>{complexity?.time}</strong>
|
||||
</span>
|
||||
<span>
|
||||
{t("SpaceComplexity")} <strong>{complexity?.space}</strong>
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
50
src/features/problems/detail/components/content.tsx
Normal file
50
src/features/problems/detail/components/content.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||
import { DetailTable } from "@/features/problems/detail/components/table";
|
||||
|
||||
interface DetailContentProps {
|
||||
submissionId: string;
|
||||
}
|
||||
|
||||
export const DetailContent = ({ submissionId }: DetailContentProps) => {
|
||||
return (
|
||||
<ScrollArea className="h-full">
|
||||
<DetailTable submissionId={submissionId} />
|
||||
<ScrollBar orientation="horizontal" />
|
||||
</ScrollArea>
|
||||
);
|
||||
};
|
||||
|
||||
export const DetailContentSkeleton = () => {
|
||||
return (
|
||||
<div className="relative h-full w-full">
|
||||
<div className="absolute h-full w-full p-4 md:p-6">
|
||||
{/* Title skeleton */}
|
||||
<Skeleton className="mb-6 h-8 w-3/4" />
|
||||
|
||||
{/* Content skeletons */}
|
||||
<Skeleton className="mb-4 h-4 w-full" />
|
||||
<Skeleton className="mb-4 h-4 w-5/6" />
|
||||
<Skeleton className="mb-4 h-4 w-2/3" />
|
||||
<Skeleton className="mb-4 h-4 w-full" />
|
||||
<Skeleton className="mb-4 h-4 w-4/5" />
|
||||
|
||||
{/* Example section heading */}
|
||||
<Skeleton className="mb-4 mt-8 h-6 w-1/4" />
|
||||
|
||||
{/* Example content */}
|
||||
<Skeleton className="mb-4 h-4 w-full" />
|
||||
<Skeleton className="mb-4 h-4 w-5/6" />
|
||||
|
||||
{/* Code block skeleton */}
|
||||
<div className="mb-6">
|
||||
<Skeleton className="h-40 w-full rounded-md" />
|
||||
</div>
|
||||
|
||||
{/* More content */}
|
||||
<Skeleton className="mb-4 h-4 w-full" />
|
||||
<Skeleton className="mb-4 h-4 w-3/4" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
97
src/features/problems/detail/components/form.tsx
Normal file
97
src/features/problems/detail/components/form.tsx
Normal file
@ -0,0 +1,97 @@
|
||||
import prisma from "@/lib/prisma";
|
||||
import {
|
||||
Accordion,
|
||||
AccordionContent,
|
||||
AccordionItem,
|
||||
AccordionTrigger,
|
||||
} from "@/components/ui/accordion";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { getTranslations } from "next-intl/server";
|
||||
|
||||
interface DetailFormProps {
|
||||
submissionId: string;
|
||||
}
|
||||
|
||||
export const DetailForm = async ({ submissionId }: DetailFormProps) => {
|
||||
const t = await getTranslations("DetailsPage");
|
||||
|
||||
const submission = await prisma.submission.findUnique({
|
||||
where: {
|
||||
id: submissionId,
|
||||
},
|
||||
});
|
||||
|
||||
const lastTestcaseResult = await prisma.testcaseResult.findFirst({
|
||||
where: { submissionId, isCorrect: false },
|
||||
orderBy: { createdAt: "desc" },
|
||||
});
|
||||
|
||||
if (!lastTestcaseResult) return null;
|
||||
|
||||
const testcase = await prisma.testcase.findUnique({
|
||||
where: { id: lastTestcaseResult.testcaseId },
|
||||
include: { inputs: true },
|
||||
});
|
||||
|
||||
if (!testcase) return null;
|
||||
|
||||
const sortedInputs = testcase.inputs?.sort((a, b) => a.index - b.index);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-col space-y-2">
|
||||
<Accordion type="single" collapsible className="w-full -space-y-px">
|
||||
<AccordionItem
|
||||
value="input"
|
||||
className="bg-background has-focus-visible:border-ring has-focus-visible:ring-ring/50 relative border px-4 py-1 outline-none first:rounded-t-md last:rounded-b-md last:border-b has-focus-visible:z-10 has-focus-visible:ring-[3px]"
|
||||
>
|
||||
<AccordionTrigger className="py-2 text-[15px] leading-6 hover:no-underline focus-visible:ring-0">
|
||||
<Label>{t("Input")}</Label>
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="text-muted-foreground pb-2">
|
||||
<div className="space-y-4">
|
||||
{sortedInputs.map((input) => (
|
||||
<div key={input.id} className="space-y-2">
|
||||
<Label>{input.name} =</Label>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={`Enter ${input.name}`}
|
||||
readOnly
|
||||
className="bg-muted border-transparent shadow-none rounded-lg h-10"
|
||||
value={input.value}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
</Accordion>
|
||||
</div>
|
||||
{lastTestcaseResult.output && (
|
||||
<div className="flex flex-col space-y-2">
|
||||
<Label>输出</Label>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={`Enter output`}
|
||||
readOnly
|
||||
className="bg-muted border-transparent shadow-none rounded-lg h-10"
|
||||
value={lastTestcaseResult.output}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{submission?.status === "WA" && (
|
||||
<div className="flex flex-col space-y-2">
|
||||
<Label>预期结果</Label>
|
||||
<Input
|
||||
type="text"
|
||||
placeholder={`Enter expected output`}
|
||||
readOnly
|
||||
className="bg-muted border-transparent shadow-none rounded-lg h-10"
|
||||
value={testcase.expectedOutput}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
42
src/features/problems/detail/components/header.tsx
Normal file
42
src/features/problems/detail/components/header.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import { Actions } from "flexlayout-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { ArrowLeftIcon } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useProblemFlexLayoutStore } from "@/stores/problem-flexlayout";
|
||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||
|
||||
export const DetailHeader = () => {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
const t = useTranslations("DetailsPage");
|
||||
const { model } = useProblemFlexLayoutStore();
|
||||
|
||||
const handleClick = () => {
|
||||
const params = new URLSearchParams(searchParams.toString());
|
||||
params.delete("submissionId");
|
||||
router.push(`${pathname}?${params.toString()}`);
|
||||
if (!model) return;
|
||||
model.doAction(Actions.selectTab("submission"));
|
||||
const detailTab = model.getNodeById("detail");
|
||||
if (detailTab) {
|
||||
model.doAction(Actions.deleteTab("detail"));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-8 flex flex-none items-center px-2 py-1 border-b">
|
||||
<Button
|
||||
onClick={handleClick}
|
||||
variant="ghost"
|
||||
className="h-8 w-auto p-2 hover:bg-transparent text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
<ArrowLeftIcon size={16} aria-hidden="true" />
|
||||
<span>{t("BackButton")}</span>
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
29
src/features/problems/detail/components/panel.tsx
Normal file
29
src/features/problems/detail/components/panel.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import { Suspense } from "react";
|
||||
import {
|
||||
DetailContent,
|
||||
DetailContentSkeleton,
|
||||
} from "@/features/problems/detail/components/content";
|
||||
import { DetailHeader } from "@/features/problems/detail/components/header";
|
||||
|
||||
interface DetailPanelProps {
|
||||
submissionId: string | undefined;
|
||||
}
|
||||
|
||||
export const DetailPanel = ({ submissionId }: DetailPanelProps) => {
|
||||
if (!submissionId) {
|
||||
return <DetailContentSkeleton />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col border border-t-0 border-muted rounded-b-lg bg-background overflow-hidden">
|
||||
<DetailHeader />
|
||||
<div className="relative flex-1">
|
||||
<div className="absolute h-full w-full">
|
||||
<Suspense fallback={<DetailContentSkeleton />}>
|
||||
<DetailContent submissionId={submissionId} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
95
src/features/problems/detail/components/table.tsx
Normal file
95
src/features/problems/detail/components/table.tsx
Normal file
@ -0,0 +1,95 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import prisma from "@/lib/prisma";
|
||||
import { Locale } from "@/generated/client";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { formatSubmissionDate } from "@/config/locale";
|
||||
import { getLabelForLanguage } from "@/config/language";
|
||||
import { getColorClassForStatus } from "@/config/status";
|
||||
import { PreDetail } from "@/components/content/pre-detail";
|
||||
import { ViewSolutionButton } from "./view-solution-button";
|
||||
import { getLocale, getTranslations } from "next-intl/server";
|
||||
import { MdxRenderer } from "@/components/content/mdx-renderer";
|
||||
import { MdxComponents } from "@/components/content/mdx-components";
|
||||
import { DetailForm } from "@/features/problems/detail/components/form";
|
||||
import { AnalyzeButton } from "@/features/problems/detail/components/analyze-button";
|
||||
|
||||
interface DetailTableProps {
|
||||
submissionId: string;
|
||||
}
|
||||
|
||||
export const DetailTable = async ({ submissionId }: DetailTableProps) => {
|
||||
const t = await getTranslations("DetailsPage");
|
||||
const s = await getTranslations("StatusMessage");
|
||||
const locale = (await getLocale()) as Locale;
|
||||
const submission = await prisma.submission.findUnique({
|
||||
where: {
|
||||
id: submissionId,
|
||||
},
|
||||
});
|
||||
|
||||
if (!submission)
|
||||
return (
|
||||
<div className="h-full flex items-center justify-center">
|
||||
No Submission
|
||||
</div>
|
||||
);
|
||||
|
||||
const createdAt = new Date(submission.createdAt);
|
||||
const submittedDisplay = formatSubmissionDate(createdAt, locale);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col mx-auto max-w-[700px] gap-4 px-4 py-3">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="flex flex-1 flex-col items-start gap-1 overflow-hidden">
|
||||
<h3
|
||||
className={cn(
|
||||
"flex items-center text-xl font-semibold",
|
||||
getColorClassForStatus(submission.status)
|
||||
)}
|
||||
>
|
||||
<span>{s(submission.status)}</span>
|
||||
</h3>
|
||||
<div className="flex max-w-full flex-1 items-center gap-1 overflow-hidden text-xs">
|
||||
<span className="whitespace-nowrap mr-1">{t("Time")}</span>
|
||||
<span className="max-w-full truncate">{submittedDisplay}</span>
|
||||
</div>
|
||||
</div>
|
||||
<ViewSolutionButton />
|
||||
</div>
|
||||
<div className="flex flex-col gap-4">
|
||||
{submission.message && (
|
||||
<MdxRenderer
|
||||
source={`\`\`\`shell\n${submission.message}\n\`\`\``}
|
||||
/>
|
||||
)}
|
||||
<DetailForm submissionId={submissionId} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Label>{t("Code")}</Label>
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="h-4 bg-muted-foreground"
|
||||
/>
|
||||
<Label>{getLabelForLanguage(submission.language)}</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<AnalyzeButton value={submission.content} />
|
||||
</div>
|
||||
</div>
|
||||
<MdxRenderer
|
||||
source={`\`\`\`${submission.language}\n${submission.content}\n\`\`\``}
|
||||
components={{
|
||||
...MdxComponents,
|
||||
pre: PreDetail,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -0,0 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import { Actions } from "flexlayout-react";
|
||||
import { BookOpenIcon } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useProblemFlexLayoutStore } from "@/stores/problem-flexlayout";
|
||||
|
||||
export const ViewSolutionButton = () => {
|
||||
const { model } = useProblemFlexLayoutStore();
|
||||
|
||||
const handleClick = () => {
|
||||
if (!model) return;
|
||||
model.doAction(Actions.selectTab("solution"));
|
||||
};
|
||||
|
||||
return (
|
||||
<Button variant="outline" onClick={handleClick}>
|
||||
<BookOpenIcon />
|
||||
查看题解
|
||||
</Button>
|
||||
);
|
||||
};
|
@ -10,7 +10,7 @@ interface SolutionPanelProps {
|
||||
|
||||
export const SolutionPanel = ({ problemId }: SolutionPanelProps) => {
|
||||
return (
|
||||
<div className="h-full flex flex-col border border-t-0 border-muted rounded-b-3xl bg-background overflow-hidden">
|
||||
<div className="h-full flex flex-col border border-t-0 border-muted rounded-b-lg bg-background overflow-hidden">
|
||||
<div className="relative flex-1">
|
||||
<div className="absolute h-full w-full">
|
||||
<Suspense fallback={<SolutionContentSkeleton />}>
|
||||
|
88
src/features/problems/submission/components/content.tsx
Normal file
88
src/features/problems/submission/components/content.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
import { auth, signIn } from "@/lib/auth";
|
||||
import { CodeXmlIcon } from "lucide-react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
||||
import { SubmissionTable } from "@/features/problems/submission/components/table";
|
||||
|
||||
interface SubmissionContentProps {
|
||||
problemId: string;
|
||||
}
|
||||
|
||||
const LoginPromptCard = () => {
|
||||
const t = useTranslations("LoginPromptCard");
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col items-center justify-start gap-2 pt-[10vh]">
|
||||
<div className="flex items-center gap-3">
|
||||
<CodeXmlIcon
|
||||
className="shrink-0 text-blue-500"
|
||||
size={16}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<p className="text-base font-medium">{t("title")}</p>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">{t("description")}</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="bg-muted hover:bg-muted/80"
|
||||
onClick={async () => {
|
||||
"use server";
|
||||
await signIn();
|
||||
}}
|
||||
>
|
||||
{t("loginButton")}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const SubmissionContent = async ({
|
||||
problemId,
|
||||
}: SubmissionContentProps) => {
|
||||
const session = await auth();
|
||||
const userId = session?.user?.id;
|
||||
|
||||
return (
|
||||
<ScrollArea className="h-full px-3">
|
||||
{userId ? <SubmissionTable problemId={problemId} /> : <LoginPromptCard />}
|
||||
<ScrollBar orientation="horizontal" />
|
||||
</ScrollArea>
|
||||
);
|
||||
};
|
||||
|
||||
export const SubmissionContentSkeleton = () => {
|
||||
return (
|
||||
<div className="relative h-full w-full">
|
||||
<div className="absolute h-full w-full p-4 md:p-6">
|
||||
{/* Title skeleton */}
|
||||
<Skeleton className="mb-6 h-8 w-3/4" />
|
||||
|
||||
{/* Content skeletons */}
|
||||
<Skeleton className="mb-4 h-4 w-full" />
|
||||
<Skeleton className="mb-4 h-4 w-5/6" />
|
||||
<Skeleton className="mb-4 h-4 w-2/3" />
|
||||
<Skeleton className="mb-4 h-4 w-full" />
|
||||
<Skeleton className="mb-4 h-4 w-4/5" />
|
||||
|
||||
{/* Example section heading */}
|
||||
<Skeleton className="mb-4 mt-8 h-6 w-1/4" />
|
||||
|
||||
{/* Example content */}
|
||||
<Skeleton className="mb-4 h-4 w-full" />
|
||||
<Skeleton className="mb-4 h-4 w-5/6" />
|
||||
|
||||
{/* Code block skeleton */}
|
||||
<div className="mb-6">
|
||||
<Skeleton className="h-40 w-full rounded-md" />
|
||||
</div>
|
||||
|
||||
{/* More content */}
|
||||
<Skeleton className="mb-4 h-4 w-full" />
|
||||
<Skeleton className="mb-4 h-4 w-3/4" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
23
src/features/problems/submission/components/panel.tsx
Normal file
23
src/features/problems/submission/components/panel.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { Suspense } from "react";
|
||||
import {
|
||||
SubmissionContent,
|
||||
SubmissionContentSkeleton,
|
||||
} from "@/features/problems/submission/components/content";
|
||||
|
||||
interface SubmissionPanelProps {
|
||||
problemId: string;
|
||||
}
|
||||
|
||||
export const SubmissionPanel = ({ problemId }: SubmissionPanelProps) => {
|
||||
return (
|
||||
<div className="h-full flex flex-col border border-t-0 border-muted rounded-b-lg bg-background overflow-hidden">
|
||||
<div className="relative flex-1">
|
||||
<div className="absolute h-full w-full">
|
||||
<Suspense fallback={<SubmissionContentSkeleton />}>
|
||||
<SubmissionContent problemId={problemId} />
|
||||
</Suspense>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user