feat: task

This commit is contained in:
mehedi-hasan 2024-04-07 22:53:52 +06:00
parent f7ad1ac2ab
commit ca0e7f71b3
12 changed files with 724 additions and 0 deletions

View File

@ -0,0 +1,19 @@
import { Metadata } from "next";
import React from "react";
import Header from "@/components/task/header";
export const metadata: Metadata = {
title: "Task | Skilled Ai",
description: "Skilled Ai",
};
const Layout = ({ children }: { children: React.ReactNode }) => {
return (
<div className="grid grid-rows-[53px_1fr] min-h-screen">
<Header/>
<main className="">{children}</main>
</div>
);
};
export default Layout;

View File

@ -0,0 +1,170 @@
import React from "react";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable";
import { unified } from "unified";
import remarkParse from "remark-parse";
import remarkRehype from "remark-rehype";
import rehypeStringify from "rehype-stringify";
import rehypePrettyCode from "rehype-pretty-code";
import markdownStyles from "@/styles/markdown-styles.module.css";
import { tutorialData } from "@/constants/tutorial-data";
import { notFound } from "next/navigation";
import { buttonVariants } from "@/components/ui/button";
import Link from "next/link";
import { cn } from "@/lib/utils";
import EditorWindow from "@/components/task/editor-window";
import { LANGUAGE_VERSIONS } from "@/constants/language-options";
async function main(tutorialCode: string) {
const file = await unified()
.use(remarkParse)
.use(remarkRehype)
.use(rehypePrettyCode, {})
.use(rehypeStringify)
.process(tutorialCode);
return file;
}
const languages = Object.keys(LANGUAGE_VERSIONS);
const Page = async ({ params }: { params: { taskId: string[] } }) => {
if (!params.taskId[0] || !languages.includes(params.taskId[0])) {
notFound();
}
const langTutorial = tutorialData.find(
(t) => t.language === params.taskId[0],
)!;
const tutorial = langTutorial.tutorial.find(
(t) => t.id === (Number(params.taskId[1]) || 1),
);
const tutorialIndex = langTutorial.tutorial.findIndex(
(t) => t.id === (Number(params.taskId[1]) || 1),
);
if (!tutorial || tutorialIndex === -1) {
notFound();
}
const code = await main(tutorial?.content || "");
return (
<>
<div className="md:hidden w-full h-full">
<ResizablePanelGroup
direction="vertical"
className="max-w-[1440px] mx-auto h-full"
>
<ResizablePanel defaultSize={50}>
<ScrollArea className="h-[calc(100vh-55px)] border-b">
<div className="px-4 py-6 flex flex-col gap-4 h-full mb-[50vh]">
<div
className={cn("grow", markdownStyles["markdown"])}
dangerouslySetInnerHTML={{ __html: String(code) }}
></div>
<div className="flex justify-between">
<div className="mb-6">
{langTutorial?.tutorial[tutorialIndex - 1] && (
<Link
href={`/dashboard/task/${params.taskId[0]}/${
Number(params.taskId[1]) - 1
}`}
className={buttonVariants({
variant: "outline",
})}
>
Prev
</Link>
)}
</div>
<div className="mb-6">
{langTutorial?.tutorial[tutorialIndex + 1] && (
<Link
href={`/dashboard/task/${params.taskId[0]}/${
Number(params.taskId[1]) + 1
}`}
className={buttonVariants({ variant: "outline" })}
>
Next
</Link>
)}
</div>
</div>
</div>
</ScrollArea>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={50}>
<EditorWindow
language={params.taskId[0]}
step={Number(params.taskId[1]) || 1}
/>
</ResizablePanel>
</ResizablePanelGroup>
</div>
<div className="hidden md:block w-full h-full">
<ResizablePanelGroup
direction="horizontal"
className="max-w-[1440px] mx-auto h-full"
>
<ResizablePanel defaultSize={50}>
<ScrollArea className="h-[calc(100vh-55px)]">
<div className="px-4 py-6 flex flex-col gap-4 h-full">
<div
className={cn("grow", markdownStyles["markdown"])}
dangerouslySetInnerHTML={{ __html: String(code) }}
></div>
<div className="flex justify-between">
<div className="mb-6">
{langTutorial?.tutorial[tutorialIndex - 1] && (
<Link
href={`/dashboard/task/${params.taskId[0]}/${
Number(params.taskId[1]) - 1
}`}
className={buttonVariants({
variant: "outline",
})}
>
Prev
</Link>
)}
</div>
<div className="mb-6">
{langTutorial?.tutorial[tutorialIndex + 1] && (
<Link
href={`/dashboard/task/${params.taskId[0]}/${
Number(params.taskId[1]) + 1
}`}
className={buttonVariants({ variant: "outline" })}
>
Next
</Link>
)}
</div>
</div>
</div>
</ScrollArea>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={50}>
<EditorWindow
language={params.taskId[0]}
step={Number(params.taskId[1]) || 1}
/>
</ResizablePanel>
</ResizablePanelGroup>
</div>
</>
);
};
export default Page;

View File

@ -0,0 +1,135 @@
import React, { useCallback } from "react";
import Editor from "@monaco-editor/react";
const activateMonacoJSXHighlighter = async (monacoEditor: any, monaco: any) => {
// monaco-jsx-highlighter depends on these in addition to Monaco and an instance of a Monaco Editor.
const { default: traverse } = await import("@babel/traverse");
const { parse } = await import("@babel/parser");
// >>> The star of the show =P >>>
const {
default: MonacoJSXHighlighter,
JSXTypes,
makeBabelParse, //By @HaimCandiTech
} = await import(
// @ts-ignore
"monaco-jsx-highlighter" // Note: there is a polyfilled version alongside the regular version.
); // For example, starting with 2.0.2, 2.0.2-polyfilled is also available.
const parseJSX = makeBabelParse(parse, true); // param0:Babel's parse, param1: default config for JSX syntax (false), TSX (true).
// Instantiate the highlighter
const monacoJSXHighlighter = new MonacoJSXHighlighter(
monaco, // references Range and other APIs
parseJSX, // obtains an AST, internally passes to parse options: {...options, sourceType: "module",plugins: ["jsx"],errorRecovery: true}
traverse, // helps collecting the JSX expressions within the AST
monacoEditor, // highlights the content of that editor via decorations
);
// Start the JSX highlighting and get the dispose function
let disposeJSXHighlighting =
monacoJSXHighlighter.highlightOnDidChangeModelContent();
// Enhance monaco's editor.action.commentLine with JSX commenting and get its disposer
let disposeJSXCommenting = monacoJSXHighlighter.addJSXCommentCommand();
// <<< You are all set. >>>
// Optional: customize the color font in JSX texts (style class JSXElement.JSXText.tastyPizza from ./index.css)
JSXTypes.JSXText.options.inlineClassName = "JSXElement.JSXText.tastyPizza";
// more details here: https://microsoft.github.io/monaco-editor/api/interfaces/monaco.editor.IModelDecorationOptions.html
// console.log(
// "Customize each JSX expression type's options, they must match monaco.editor.IModelDecorationOptions:",
// JSXTypes,
// );
// This example's shorthands for toggling actions
const toggleJSXHighlighting = () => {
if (disposeJSXHighlighting) {
disposeJSXHighlighting();
disposeJSXHighlighting = null;
return false;
}
disposeJSXHighlighting =
monacoJSXHighlighter.highlightOnDidChangeModelContent();
return true;
};
const toggleJSXCommenting = () => {
if (disposeJSXCommenting) {
disposeJSXCommenting();
disposeJSXCommenting = null;
return false;
}
disposeJSXCommenting = monacoJSXHighlighter.addJSXCommentCommand();
return true;
};
const isToggleJSXHighlightingOn = () => !!disposeJSXHighlighting;
const isToggleJSXCommentingOn = () => !!disposeJSXCommenting;
return {
monacoJSXHighlighter,
toggleJSXHighlighting,
toggleJSXCommenting,
isToggleJSXHighlightingOn,
isToggleJSXCommentingOn,
};
};
type CodeEditorProps = {
// code: string;
defaultCode?: string;
onChange: (value: string | undefined) => void;
theme: string;
language: string;
codeValue: string;
};
export function CodeEditor({
defaultCode,
onChange,
theme,
language,
codeValue,
}: CodeEditorProps) {
// const [value, setValue] = useState("");
const handleEditorDidMount = useCallback((monacoEditor: any, monaco: any) => {
// monacoEditor.updateOptions({ wordWrap: "on" });
activateMonacoJSXHighlighter(monacoEditor, monaco)
.then((monacoJSXHighlighterRefCurrent) => {
// monacoJSXHighlighterRef.current = monacoJSXHighlighterRefCurrent;
// setIsEditorReady(!!monacoEditor);
// setIsJSXHighlightingOn(
// monacoJSXHighlighterRefCurrent.isToggleJSXHighlightingOn()
// );
// setIsJSXCommentingOn(
// monacoJSXHighlighterRefCurrent.isToggleJSXCommentingOn()
// );
})
.catch((e) => {
// console.log(e)
});
}, []);
// const handleEditorChange = (value: string | undefined) => {
// setValue(value || "");
// onChange(value || "");
// };
return (
<Editor
// height="50vh" // By default, it fully fits with its parent
options={{
minimap: {
enabled: false,
},
wordWrap: "on"
}}
theme={theme}
language={language === "react" ? "javascript" : language}
value={codeValue}
defaultValue={defaultCode}
onMount={handleEditorDidMount}
onChange={onChange}
/>
);
}

View File

@ -0,0 +1,27 @@
//@ts-ignore
import { transform } from "@babel/standalone";
const compile = (input: string) =>
transform(input, {
filename: "random.tsx",
presets: ["react", "es2017"]
})?.code;
export const compileCode = (code: string) => {
// try {
const compiled = compile(
code.replace(
"import * as React from 'react'; //don't change this line\n",
""
)
)?.replace('"use strict";\n', "");
//@ts-ignore
return new Function("React", "", `return ${compiled};`);
// } catch (error) {
// console.log(error)
// // return new Function("React", "", `return <div></div>;`);
// }
};

View File

@ -0,0 +1,150 @@
"use client";
import React, { useEffect, useState } from "react";
import { compileCode } from "./compiler";
import { v4 as uuid } from "uuid";
import { CodeEditor } from "./code-editor";
import { ErrorBoundary } from "react-error-boundary";
import PreviewErrorFallback from "./preview-error-fallback";
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable";
import { ThemeDropdown } from "./theme-dropdown";
import LanguagesDropdown from "./languages-dropdown";
import Output from "./output";
import { tutorialData } from "@/constants/tutorial-data";
import { notFound, useRouter } from "next/navigation";
import { defineTheme } from "@/utils/define-theme";
import Preview from "./preview";
import { MyComponents } from "./my-components";
export default function EditorWindow({
language: lang,
step,
}: {
language: string;
step: number;
}) {
const [componentName, setComponentName] = useState<any>("");
const [theme, setTheme] = useState("cobalt");
// const [language, setLanguage] = useState(languageOptions[0]);
// const [language, setLanguage] = useState("react");
// const [defaultCode, setDefaultCode] = useState("");
const [codeValue, setCodeValue] = useState("");
const router = useRouter();
const handleCodeChange = (code: string | undefined) => {
try {
setCodeValue(code || "");
const func = compileCode(code || "");
const id = uuid();
MyComponents[id] = func(React);
setComponentName(id);
} catch (err) {
console.log(err);
// setComponentName("error");
}
};
useEffect(() => {
defineTheme("oceanic-next").then((_) => setTheme("oceanic-next"));
}, []);
function handleThemeChange(th: any) {
const theme = th;
// console.log("theme...", theme);
if (["light", "vs-dark"].includes(theme)) {
setTheme(theme);
} else {
defineTheme(theme).then((_) => setTheme(theme));
}
}
const handleLanguageChange = (lang: string) => {
router.push(`/dashboard/task/${lang}/1`);
// try {
// if (lang === "react") {
// const func = compileCode(CODE_SNIPPETS[lang]);
// const id = uuid();
// MyComponents[id] = func(React);
// setComponentName(id);
// }
// setLanguage(lang);
// // @ts-ignore
// setCodeValue(CODE_SNIPPETS[lang]);
// // @ts-ignore
// // setDefaultCode(CODE_SNIPPETS[language]);
// } catch (error) {
// // console.log(error);
// }
};
const tutorial = tutorialData
.find((t) => t.language === lang)
?.tutorial.find((t) => t.id === step);
if (!tutorial) {
notFound();
}
// console.log(tutorial);
return (
<ResizablePanelGroup direction="vertical" className="border-r">
<ResizablePanel defaultSize={50}>
<div className="h-full ">
<div className="p-2 flex gap-4">
<ThemeDropdown
handleThemeChange={handleThemeChange}
theme={theme}
/>
<LanguagesDropdown
handleLanguageChange={handleLanguageChange}
language={lang}
/>
</div>
<div className="h-full">
<CodeEditor
codeValue={codeValue}
// @ts-ignore
// defaultCode={CODE_SNIPPETS[language]}
defaultCode={tutorial?.code || ""}
onChange={handleCodeChange}
theme={theme}
// language={language}
language={lang}
/>
</div>
</div>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={50}>
<ErrorBoundary FallbackComponent={PreviewErrorFallback}>
<div className="bg-white text-black h-full p-4">
{lang === "react" ? (
<Preview
componentName={componentName}
tutorialCode={tutorial?.code || ""}
/>
) : (
<Output
// @ts-ignore
codeValue={codeValue || tutorial?.code || ""}
// codeValue={codeValue || CODE_SNIPPETS[language]}
// language={language}
language={lang}
/>
)}
</div>
</ErrorBoundary>
</ResizablePanel>
</ResizablePanelGroup>
);
}

View File

@ -0,0 +1,24 @@
import ThemeToggle from "@/components/layout/ThemeToggle/theme-toggle";
import Link from "next/link";
import React from "react";
const Header = () => {
return (
<header className="border-b px-4 h-min">
<nav className="flex justify-between items-center max-w-[1440px] mx-auto py-2">
<Link href="/">
<h2>Skilled Ai</h2>
</Link>
<div className="flex items-center gap-8">
{/* <Link href="/">Home</Link> */}
<Link href="/dashboard">Dashboard</Link>
{/* <Link href="/login">Login</Link> */}
<ThemeToggle/>
</div>
</nav>
</header>
);
};
export default Header;

View File

@ -0,0 +1,42 @@
import React from "react";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { LANGUAGE_VERSIONS } from "@/constants/language-options";
// import { ScrollArea } from "@/components/ui/scroll-area";
// import Link from "next/link";
const languages = Object.entries(LANGUAGE_VERSIONS);
const LanguagesDropdown = ({ handleLanguageChange, language }: any) => {
return (
<Select onValueChange={handleLanguageChange} value={language}>
<SelectTrigger className="w-[150px]">
<SelectValue placeholder="Select Language" />
</SelectTrigger>
<SelectContent>
{/* <ScrollArea className="h-96"> */}
<SelectGroup>
<SelectLabel>Languages</SelectLabel>
{languages.map(([lang, version]) => (
// <Link href={`/dashboard/task/${lang}`} key={lang}>
<SelectItem className="cursor-pointer" value={lang} key={lang}>
{lang} {version}
</SelectItem>
// </Link>
))}
</SelectGroup>
{/* </ScrollArea> */}
</SelectContent>
</Select>
);
};
export default LanguagesDropdown;

View File

@ -0,0 +1,13 @@
import React from "react";
import { compileCode } from "./compiler";
import { CODE_SNIPPETS } from "@/constants/code-snippets";
export const MyComponents: { [key: string]: any } = {
default: (() => {
const func = compileCode(CODE_SNIPPETS["react"]);
return func(React);
})(),
error: function () {
return <div>Error</div>;
},
};

View File

@ -0,0 +1,53 @@
import { Button } from "@/components/ui/button";
import { useState } from "react";
import { cn } from "@/lib/utils";
import { useToast } from "@/components/ui/use-toast";
import { executeCode } from "@/lib/api";
const Output = ({ codeValue, language }: any) => {
const { toast } = useToast();
const [output, setOutput] = useState<any>(null);
const [isLoading, setIsLoading] = useState(false);
const [isError, setIsError] = useState(false);
const runCode = async () => {
if (!codeValue) return;
try {
setIsLoading(true);
const { run: result } = await executeCode(language, codeValue);
setOutput(result.output.split("\n"));
result.stderr ? setIsError(true) : setIsError(false);
} catch (error) {
// console.log(error);
toast({
variant: "destructive",
title: "An error occurred.",
description: (error as Error).message || "Unable to run code",
});
} finally {
setIsLoading(false);
}
};
return (
<div className="p-4">
{/* <p>Output</p> */}
<Button variant="outline" onClick={runCode} disabled={isLoading}>
{isLoading ? "Running..." : "Run Code"}
</Button>
<div
className={cn(
"mt-4 p-4 rounded-md",
isError ? "text-red-700 border border-red-700 bg-red-50" : "border",
)}
// color={isError ? "red.400" : ""}
// borderColor={isError ? "red.500" : "#333"}
>
{output
? output.map((line: any, i: number) => <p key={i}>{line}</p>)
: 'Click "Run Code" to see the output here'}
</div>
</div>
);
};
export default Output;

View File

@ -0,0 +1,24 @@
"use client";
import { Button } from "@/components/ui/button";
import React from "react";
// import { useErrorBoundary } from "react-error-boundary";
const PreviewErrorFallback = ({ error, resetErrorBoundary }: any) => {
// const { resetBoundary } = useErrorBoundary();
// console.log(error);
return (
<div className="bg-white text-black p-4 h-full">
<div className="border border-red-700 text-red-700 bg-red-50 mb-4 p-4 rounded-md">
<p className="mb-2 font-semibold">Something went wrong!</p>
<p>{error.message}</p>
</div>
<Button variant="outline" onClick={resetErrorBoundary}>
Reset
</Button>
</div>
);
};
export default PreviewErrorFallback;

View File

@ -0,0 +1,23 @@
import React from "react";
import { compileCode } from "./compiler";
import { MyComponents } from "./my-components";
export default function Preview({
componentName,
tutorialCode,
}: {
componentName: string;
tutorialCode: string;
}) {
// const Component =
// componentName && MyComponents[componentName] ? (
// MyComponents[componentName]
// ) : (
// <></>
// );
const Component =
componentName !== ""
? MyComponents[componentName]
: compileCode(tutorialCode)(React);
return <Component />;
}

View File

@ -0,0 +1,44 @@
import * as React from "react";
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
// @ts-ignore
import monacoThemes from "monaco-themes/themes/themelist";
import { ScrollArea } from "@/components/ui/scroll-area";
export function ThemeDropdown({ handleThemeChange, theme }: any) {
const themes = Object.entries(monacoThemes)
// .filter(([themeId, themeName]) => themeId !== "cobalt2")
.map(([themeId, themeName]) => ({
label: themeName,
value: themeId,
key: themeId,
}));
return (
<Select onValueChange={handleThemeChange} value={theme}>
<SelectTrigger className="w-[150px]">
<SelectValue placeholder="Select theme" />
</SelectTrigger>
<SelectContent>
<ScrollArea className="h-96">
<SelectGroup>
<SelectLabel>Theme</SelectLabel>
{themes.map(({ label, value, key }) => (
<SelectItem className="cursor-pointer" key={key} value={value}>
{label as string}
</SelectItem>
))}
</SelectGroup>
</ScrollArea>
</SelectContent>
</Select>
);
}