feat: replace dockview with flexlayout-react

This commit is contained in:
cfngc4594 2025-06-13 14:03:17 +08:00
parent 03f150214d
commit 941f1a74fa
119 changed files with 5091 additions and 4107 deletions

View File

@ -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

View File

@ -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=="],

View File

@ -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."
}
}

View File

@ -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 视频。"
}
}

View File

@ -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",

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "password" TEXT;

View File

@ -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)

File diff suppressed because it is too large Load Diff

BIN
public/sign-in.mp4 Normal file

Binary file not shown.

View File

@ -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;
}

View File

@ -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 },
});
}
};

View File

@ -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>
);
}

View File

@ -1,3 +0,0 @@
export default function DashboardAdmin() {
return <div>Dashboard Admin</div>;
}

View File

@ -1,5 +0,0 @@
import NewProblemDescriptionForm from "@/components/features/dashboard/admin/problemset/new/components/description-form";
export default function NewProblemDescriptionPage() {
return <NewProblemDescriptionForm />;
}

View File

@ -1,5 +0,0 @@
import NewProblemMetadataForm from "@/components/features/dashboard/admin/problemset/new/components/metadata-form";
export default function NewProblemMetadataPage() {
return <NewProblemMetadataForm />;
}

View File

@ -1,5 +0,0 @@
import { redirect } from "next/navigation";
export default function NewProblemPage() {
redirect("/dashboard/problemset/new/metadata");
}

View File

@ -1,5 +0,0 @@
import NewProblemSolutionForm from "@/components/features/dashboard/admin/problemset/new/components/solution-form";
export default function NewProblemSolutionPage() {
return <NewProblemSolutionForm />;
}

View File

@ -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);
}
},
}
)
);

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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} />;
}

View File

@ -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();
}

View File

@ -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>
);
}

View File

@ -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>
</>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
</>
);
}

View File

@ -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>
);
}

View File

@ -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>
</>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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",
},
},
]}
/>
);
}

View File

@ -19,7 +19,9 @@ export default async function ProblemLayout({
return (
<div className="flex flex-col h-screen">
<ProblemHeader />
<div className="flex w-full flex-grow overflow-y-hidden p-2.5 pt-0">
{children}
</div>
</div>
);
}

View 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
View 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>
);
}

View File

@ -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,7 +32,6 @@ 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
@ -50,10 +49,11 @@ export default async function SignInPage({ searchParams }: SignInPageProps) {
<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">
<p className="text-balance text-sm text-muted-foreground">
{t("description")}
</p>
</div>
<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")}
@ -66,7 +66,7 @@ export default async function SignInPage({ searchParams }: SignInPageProps) {
action={async () => {
"use server";
await signIn(provider.id, {
redirectTo: callbackUrl ?? "",
redirectTo: callbackUrl,
});
}}
>
@ -81,18 +81,22 @@ export default async function SignInPage({ searchParams }: SignInPageProps) {
</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>
);
}

View 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>
);
}

View 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
View 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
View 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
View 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
View 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
View 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
);
};

View File

@ -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();

View File

@ -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);

View File

@ -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>
);

View 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>
);
};

View 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),
},
});
};

View 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>
);
}

View 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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>;

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>

View File

@ -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)}
/>
);
}

View File

@ -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>
);
}

View File

@ -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}
/>
);
}
};

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}
};

View File

@ -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
View 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";
}
};

View File

@ -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,
};

View File

@ -1,3 +0,0 @@
import { EditorLanguage } from "@/generated/client";
export const DEFAULT_EDITOR_LANGUAGE = EditorLanguage.c;

View File

@ -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
View 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
View 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";
}
};

View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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>
);
};

View File

@ -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 = () => {

View File

@ -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>
);
}
};

View File

@ -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>
);
};

View File

@ -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>
</>
);
};

View File

@ -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 />

View File

@ -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) => (
{LANGUAGES.map((language) => {
const Icon = getIconForLanguage(language);
const label = getLabelForLanguage(language);
return (
<SelectItem key={language} value={language}>
{getIconForLanguage(language)}
<Icon size={16} aria-hidden="true" />
<span className="truncate text-sm font-semibold mr-2">
{getLabelForLanguage(language)}
{label}
</span>
</SelectItem>
))}
);
})}
</SelectContent>
</Select>
);

View File

@ -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>
);
};

View File

@ -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 };

View File

@ -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

View File

@ -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);

View File

@ -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 };

View 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;
}
};

View File

@ -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 />}>

View 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>
</>
);
};

View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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>
);
};

View 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>
);
};

View File

@ -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>
);
};

View File

@ -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 />}>

View 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>
);
};

View 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