|
import React, { useRef, useEffect, useState, ChangeEvent, FormEvent } from "react"; |
|
import { Message, useChat } from "ai/react"; |
|
import { useAppDispatch, useAppSelector } from "../../store/hook"; |
|
import { |
|
compiler_state, |
|
initEsbuild, |
|
} from "../../store/features/compilerSlice"; |
|
import { editor_state, set_monaco_input_value, update_editor_code } from "../../store/features/editorSlice"; |
|
import { theme_state } from "../../store/features/themeSlice"; |
|
import { ModalEnum, open_modal } from "../../store/features/modalSlice"; |
|
|
|
import ConsoleLog from "./ConsoleLog"; |
|
import Iframe from "./Iframe"; |
|
import InputCodeTab from "./InputCodeTab"; |
|
import Footer from "./Footer"; |
|
import Pane from "./Pane"; |
|
import { SendIcon } from "../../constants/icon"; |
|
import ReactMarkdown from "react-markdown"; |
|
import { ChatRequestOptions } from "ai"; |
|
|
|
const DEFAULT_PROMPT = `You are assisting in a WASM-powered live editor, Codetree, where React code is instantly reflected in the browser. Generate React code with the following constraints: |
|
1. All logic should be encapsulated within a single app component file. |
|
2. External dependencies or imports outside of the React core are not permitted. |
|
3. Always use inline styles for styling. |
|
4. Code snippets should conclude with rendering to the DOM's root element: ReactDOM.render(<App />, document.getElementById('root')); |
|
5. Ensure the code is optimized for a live browser environment and adheres strictly to React best practices.`; |
|
|
|
const Playground = () => { |
|
const dispatch = useAppDispatch(); |
|
const formRef = useRef<HTMLFormElement>(null); |
|
const inputRef = useRef<HTMLTextAreaElement>(null); |
|
const { theme } = useAppSelector(theme_state); |
|
const { esbuildStatus, isCompiling, output } = useAppSelector(compiler_state); |
|
const { logs, editorValue, isLogTabOpen } = useAppSelector(editor_state); |
|
const [markdownCode, setMarkdownCode] = useState(''); |
|
const [prevMarkdownCode, setPrevMarkdownCode] = useState(markdownCode); |
|
const [isSystemInputVisible, setSystemInputVisible] = useState(false); |
|
const [isModelInputVisible, setModelInputVisible] = useState(false); |
|
const [aiProvider, setAIProvider] = useState<string>("openai"); |
|
const [urlOption, setUrlOption] = useState<string | any>(null); |
|
|
|
const [systemMessage, setSystemMessage] = useState( |
|
DEFAULT_PROMPT |
|
); |
|
|
|
const isValidCodeBlock = (markdownCode: string) => { |
|
return markdownCode && markdownCode.length > 10 && markdownCode.includes('\n'); |
|
} |
|
|
|
const modifiedHandleSubmit = async (e: FormEvent<HTMLFormElement>, chatRequestOptions?: ChatRequestOptions) => { |
|
e.preventDefault(); |
|
await handleSubmit(e, { |
|
...chatRequestOptions, |
|
aiProvider, |
|
} as any); |
|
}; |
|
|
|
useEffect(() => { |
|
const timer = setInterval(() => { |
|
if (isValidCodeBlock(markdownCode) && markdownCode !== prevMarkdownCode) { |
|
dispatch(update_editor_code({ type: 'javascript', content: markdownCode })); |
|
setPrevMarkdownCode(markdownCode); |
|
} |
|
}, 2000); |
|
|
|
return () => { |
|
clearInterval(timer); |
|
}; |
|
}, [markdownCode, prevMarkdownCode, dispatch]); |
|
|
|
const { append, messages, input, setInput, handleSubmit, ...rest } = useChat({ |
|
body: { |
|
systemMessage: systemMessage, |
|
aiProvider: aiProvider, |
|
url: urlOption |
|
}, |
|
onError: (error) => { |
|
console.error(error); |
|
}, |
|
}); |
|
|
|
useEffect(() => { |
|
if (!esbuildStatus.isReady) { |
|
dispatch(initEsbuild()); |
|
} |
|
}, [dispatch, esbuildStatus]); |
|
|
|
useEffect(() => { |
|
dispatch(open_modal(ModalEnum.TEMPLATE)); |
|
}, [dispatch]); |
|
|
|
useEffect(() => { |
|
if(isValidCodeBlock(markdownCode)) { |
|
const newEditorValue = { |
|
name: "React", |
|
description: "By codetree", |
|
public: true, |
|
iconSrc: "/icons/reactjs.svg", |
|
tabs: { |
|
javascript: { |
|
title: "JS/JSX", |
|
entryPoints: "index.js", |
|
monacoLanguage: "javascript", |
|
data: markdownCode |
|
}, |
|
html: { |
|
title: "index.html", |
|
entryPoints: "index.html", |
|
monacoLanguage: "html", |
|
data: "" |
|
}, |
|
css: { |
|
title: "main.css", |
|
entryPoints: "main.css", |
|
monacoLanguage: "css", |
|
data: "" |
|
} |
|
} |
|
}; |
|
|
|
dispatch(set_monaco_input_value(newEditorValue as any)); |
|
} |
|
}, [markdownCode, dispatch]); |
|
|
|
return ( |
|
<div style={{ background: theme.background }}> |
|
<div className="flex flex-col"> |
|
<div className="px-4 pb-2 pt-3 shadow-lg sm:pb-3 sm:pt-4"> |
|
<span className="text-lg font-semibold">System Prompt</span> |
|
<button |
|
onClick={() => setSystemInputVisible(!isSystemInputVisible)} |
|
className="p-2 rounded-full hover:bg-gray-200 transition duration-300" |
|
> |
|
{isSystemInputVisible ? '−' : '+'} |
|
</button> |
|
<span className="text-lg font-semibold">Model</span> |
|
<button |
|
onClick={() => setModelInputVisible(!isModelInputVisible)} |
|
className="p-2 rounded-full hover:bg-gray-200 transition duration-300" |
|
> |
|
{isModelInputVisible ? '−' : '+'} |
|
</button> |
|
|
|
{isSystemInputVisible && ( |
|
<textarea |
|
className="textarea my-4 border p-2 rounded-md shadow-sm w-full resize-none" |
|
value={systemMessage || ''} |
|
onChange={e => setSystemMessage(e.target.value)} |
|
placeholder="Enter custom system prompt here" |
|
rows={2} // initial number of rows, can be adjusted |
|
style={{ overflowWrap: 'break-word', whiteSpace: 'pre-wrap' }} |
|
/> |
|
)} |
|
{isModelInputVisible && ( |
|
<div className="my-4"> |
|
<select |
|
value={aiProvider} |
|
onChange={(e) => setAIProvider(e.target.value)} |
|
className="border p-2 rounded-md shadow-sm w-full bg-transparent text-gray-700 mt-2" |
|
> |
|
<option value="openai">gpt-4</option> |
|
<option value="meta-llama/Llama-2-70b-chat-hf">meta-llama/Llama-2-70b-chat-hf</option> |
|
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor="url-option"> |
|
Model URL Option: |
|
</label> |
|
|
|
</select> |
|
<input |
|
id="url-option" |
|
type="url" |
|
placeholder="Enter URL" |
|
value={urlOption} |
|
onChange={(e) => setUrlOption(e.target.value)} |
|
className="border p-2 rounded-md shadow-sm w-full bg-transparent text-gray-700 mt-2" |
|
/> |
|
</div> |
|
)} |
|
|
|
<form |
|
ref={formRef} |
|
onSubmit={modifiedHandleSubmit} |
|
className="relative w-full" |
|
> |
|
|
|
<textarea ref={inputRef} onChange={(e) => setInput(e.target.value)} |
|
placeholder="Enter your message" |
|
onKeyDown={(e) => { |
|
if (e.key === "Enter" && !e.shiftKey) { |
|
formRef.current?.requestSubmit(); |
|
e.preventDefault(); |
|
} |
|
}} |
|
spellCheck={false} className="textarea" value={input} /> |
|
<button |
|
type="submit" |
|
className="absolute inset-y-0 right-3 my-auto flex h-8 w-8 items-center justify-center rounded-md transition-all" |
|
> |
|
<SendIcon |
|
className={"h-4 w-4"} |
|
/> |
|
</button> |
|
</form> |
|
</div> |
|
<div className="flex flex-col items-start space-y-4 overflow-y-auto max-h-[20vh]"> |
|
{messages?.map((message, index) => ( |
|
<p key={index} className="messages-text"> |
|
<ReactMarkdown |
|
className="prose mt-1 w-full break-words prose-p:leading-relaxed" |
|
components={{ |
|
a: (props) => ( |
|
<a {...props} target="_blank" rel="noopener noreferrer" /> |
|
), |
|
// @ts-ignore |
|
code: ({node, ...props}) => { |
|
const codeValue = props.children[0] || ''; |
|
setMarkdownCode(codeValue as any); |
|
|
|
return null; |
|
} |
|
}} |
|
> |
|
{message.content} |
|
</ReactMarkdown> |
|
</p> |
|
))} |
|
</div> |
|
</div> |
|
<Pane |
|
panelA={<InputCodeTab editorValue={editorValue} />} |
|
panelB={ |
|
<Iframe |
|
tabs={editorValue.tabs} |
|
output={output} |
|
isCompiling={isCompiling} |
|
esbuildStatus={esbuildStatus} |
|
/> |
|
} |
|
panelC={<ConsoleLog logs={logs} />} |
|
lastPanelVisibility={isLogTabOpen} |
|
/> |
|
|
|
<Footer /> |
|
</div> |
|
|
|
); |
|
}; |
|
|
|
export default Playground; |
|
|