pixelass's picture
feat: allow saving and running edited code
f1a6acf unverified
raw
history blame
14 kB
import { useEffect, useRef, useState } from "react";
import axios from "axios";
import AcUnitIcon from "@mui/icons-material/AcUnit";
import LocalFireDepartmentIcon from "@mui/icons-material/LocalFireDepartment";
import CheckIcon from "@mui/icons-material/Check";
import ClearIcon from "@mui/icons-material/Clear";
import CodeIcon from "@mui/icons-material/Code";
import CodeOffIcon from "@mui/icons-material/CodeOff";
import EditIcon from "@mui/icons-material/Edit";
import VisibilityIcon from "@mui/icons-material/Visibility";
import DeleteForeverIcon from "@mui/icons-material/DeleteForever";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import PlayArrowIcon from "@mui/icons-material/PlayArrow";
import ReplayIcon from "@mui/icons-material/Replay";
import MoneyIcon from "@mui/icons-material/Money";
import TollIcon from "@mui/icons-material/Toll";
import TextField from "@mui/material/TextField";
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import Accordion from "@mui/material/Accordion";
import Typography from "@mui/material/Typography";
import AccordionSummary from "@mui/material/AccordionSummary";
import AccordionDetails from "@mui/material/AccordionDetails";
import Paper from "@mui/material/Paper";
import IconButton from "@mui/material/IconButton";
import List from "@mui/material/List";
import ListItem from "@mui/material/ListItem";
import { nanoid } from "nanoid";
import AppBar from "@mui/material/AppBar";
import Toolbar from "@mui/material/Toolbar";
import ListItemIcon from "@mui/material/ListItemIcon";
import ListItemButton from "@mui/material/ListItemButton";
import ListItemText from "@mui/material/ListItemText";
import { useHost } from "esdeka/react";
import CircularProgress from "@mui/material/CircularProgress";
import CssBaseline from "@mui/material/CssBaseline";
import Slider from "@mui/material/Slider";
import { useAtom } from "jotai";
import Button from "@mui/material/Button";
import dynamic from "next/dynamic";
import FormControl from "@mui/material/FormControl";
import InputLabel from "@mui/material/InputLabel";
import Select from "@mui/material/Select";
import MenuItem from "@mui/material/MenuItem";
import { fontMono } from "@/lib/theme";
import { useColorScheme } from "@mui/material/styles";
import { getTheme, prettify } from "@/utils";
import { answersAtom, showCodeAtom } from "@/store/atoms";
import { base } from "@/constants";
import { EditTitle } from "@/components/EditTitle";
const MonacoEditor = dynamic(import("@monaco-editor/react"), { ssr: false });
export default function Home() {
const ref = useRef<HTMLIFrameElement>(null);
const [template, setTemplate] = useState(prettify(base.default));
const [runningId, setRunningId] = useState("1");
const [activeId, setActiveId] = useState("1");
const [editingId, setEditingId] = useState<string | null>(null);
const [answers, setAnswers] = useAtom(answersAtom);
const [showCode, setShowCode] = useAtom(showCodeAtom);
const [loading, setLoading] = useState(false);
const [loadingLive, setLoadingLive] = useState(true);
const { mode, systemMode } = useColorScheme();
const { call, subscribe } = useHost(ref, "fail4");
const connection = useRef(false);
const [tries, setTries] = useState(1);
// Send a connection request
useEffect(() => {
const current = answers.find(({ id }) => id === runningId);
if (connection.current || tries <= 0) {
return () => {
/* Consistency */
};
}
const timeout = setTimeout(() => {
if (current) {
call({ template: current.content });
}
setTries(tries - 1);
}, 1_000);
return () => {
clearTimeout(timeout);
};
}, [call, tries, answers, runningId]);
useEffect(() => {
if (!connection.current && loadingLive) {
const unsubscribe = subscribe(event => {
const { action } = event.data;
switch (action.type) {
case "answer":
connection.current = true;
setLoadingLive(false);
console.log("connected");
break;
default:
break;
}
});
return () => {
unsubscribe();
};
}
return () => {
/* Consistency */
};
}, [subscribe, loadingLive]);
const current = answers.find(({ id }) => id === activeId);
function reload() {
connection.current = false;
if (ref.current) {
ref.current.src = `/live?${nanoid()}`;
setLoadingLive(true);
setTries(1);
}
}
return (
<>
<CssBaseline />
<Stack
sx={{
...fontMono.style,
position: "absolute",
inset: 0,
overflow: "hidden",
flexDirection: "row",
height: "100%",
}}
>
<Stack sx={{ width: "50%", flex: 1 }}>
<AppBar position="static" elevation={0} color="default">
<Toolbar>
<Button
form="gpt-form"
type="submit"
aria-label={loading ? "Loading" : "Run"}
aria-disabled={loading}
disabled={loading}
startIcon={
loading ? <CircularProgress size={20} /> : <PlayArrowIcon />
}
>
Run
</Button>
{current?.id === editingId ? (
<EditTitle
value={current.task}
onSave={value => {
setEditingId(null);
setAnswers(previousAnswers =>
previousAnswers.map(answer_ =>
current.id === answer_.id
? {
...answer_,
task: value,
}
: answer_
)
);
}}
/>
) : (
<>
<Typography
sx={{
flex: 1,
pl: 3,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{current?.task}
</Typography>
<IconButton
onClick={() => {
if (current) {
setEditingId(current.id);
}
}}
>
<EditIcon />
</IconButton>
</>
)}
<IconButton
color="inherit"
aria-label={showCode ? "Hide Code" : "Show Code"}
onClick={() => {
setShowCode(previousState => !previousState);
}}
>
{showCode ? <CodeOffIcon /> : <CodeIcon />}
</IconButton>
<IconButton
edge="end"
color="inherit"
aria-label="Clear Prompt"
onClick={async () => {
setActiveId("1");
setRunningId("1");
setTemplate(prettify(base.default));
reload();
}}
>
<ClearIcon />
</IconButton>
</Toolbar>
</AppBar>
{showCode && (
<Box
sx={{ flex: 1 }}
onKeyDown={event => {
if (event.key === "s" && event.metaKey) {
event.preventDefault();
setAnswers(previousAnswers =>
previousAnswers.map(previousAnswer => {
console.log(previousAnswer.id, activeId);
return previousAnswer.id === activeId
? { ...previousAnswer, content: template }
: previousAnswer;
})
);
reload();
}
}}
>
<MonacoEditor
theme={getTheme(mode, systemMode)}
language="javascript"
value={template}
options={{
fontSize: 14,
}}
onChange={async value => {
console.log(value);
setTemplate(value ?? "");
}}
/>
</Box>
)}
<Stack
sx={{ flex: 1, display: showCode ? "none" : undefined, overflow: "hidden" }}
>
<Box
component="form"
id="gpt-form"
onSubmit={async event => {
event.preventDefault();
const formData = new FormData(event.target as HTMLFormElement);
const formObject = Object.fromEntries(formData);
try {
setLoading(true);
const { data } = await axios.post("/api/gpt", formObject);
const answer = data;
setAnswers(previousAnswers => [answer, ...previousAnswers]);
setRunningId(answer.id);
setActiveId(answer.id);
setTemplate(prettify(answer.content));
reload();
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
}}
>
<Paper variant="outlined" sx={{ p: 0 }}>
<Stack sx={{ p: 2, gap: 2 }}>
<TextField
multiline
fullWidth
id="prompt"
name="prompt"
label="Prompt"
placeholder="matrix code"
defaultValue="matrix code"
maxRows={6}
InputProps={{
style: fontMono.style,
}}
/>
<TextField
multiline
fullWidth
id="negativePrompt"
name="negativePrompt"
label="Negative Prompt"
placeholder="images, audio files"
maxRows={6}
InputProps={{
style: fontMono.style,
}}
/>
</Stack>
</Paper>
<Accordion disableGutters square elevation={0}>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="gtp-options-content"
id="gtp-options-header"
sx={{
bgcolor: "background.paper",
color: "text.primary",
}}
>
<Typography>Options</Typography>
</AccordionSummary>
<AccordionDetails>
<FormControl fullWidth variant="outlined" sx={{ mb: 3 }}>
<InputLabel id="gpt-model-select-label">Model</InputLabel>
<Select
labelId="gpt-model-select-label"
id="gpt-model-select"
name="model"
defaultValue="gpt-3.5-turbo"
label="Model"
>
<MenuItem value="gpt-3.5-turbo">GPT 3.5 turbo</MenuItem>
<MenuItem value="gpt-4">GPT 4</MenuItem>
</Select>
</FormControl>
<Stack
spacing={2}
direction="row"
sx={{ mb: 2 }}
alignItems="center"
>
<AcUnitIcon />
<Slider
marks
id="temperature"
name="temperature"
min={0}
max={0.8}
defaultValue={0.2}
step={0.1}
valueLabelDisplay="auto"
aria-label="Temperature"
/>
<LocalFireDepartmentIcon />
</Stack>
<Stack
spacing={2}
direction="row"
sx={{ mb: 2 }}
alignItems="center"
>
<TollIcon />
<Slider
marks
id="maxTokens"
name="maxTokens"
min={1024}
max={4096}
defaultValue={2048}
step={256}
valueLabelDisplay="auto"
aria-label="Max Tokens"
/>
<MoneyIcon />
</Stack>
<input
id="template"
name="template"
type="hidden"
value={template}
onChange={event => {
setTemplate(event.target.value);
}}
/>
</AccordionDetails>
</Accordion>
</Box>
<List sx={{ flex: 1, overflow: "auto" }}>
{answers.map((answer, index) => {
return (
<ListItem
key={answer.id}
secondaryAction={
<Stack sx={{ flexDirection: "row", gap: 1 }}>
{answer.id === "1" ? undefined : (
<IconButton
edge="end"
aria-label="Delete"
onClick={() => {
setAnswers(previousAnswers =>
previousAnswers.filter(
({ id }) => id !== answer.id
)
);
if (runningId === answer.id) {
const previous = answers[index + 1];
if (previous) {
setActiveId(previous.id);
setRunningId(previous.id);
setTemplate(
prettify(previous.task)
);
reload();
}
}
}}
>
<DeleteForeverIcon />
</IconButton>
)}
</Stack>
}
disablePadding
>
<ListItemButton
dense
selected={activeId === answer.id}
disabled={activeId === answer.id}
role={undefined}
onClick={() => {
setActiveId(answer.id);
setRunningId(answer.id);
setTemplate(prettify(answer.content));
reload();
}}
>
<ListItemIcon>
{runningId === answer.id ? (
<CheckIcon />
) : (
<VisibilityIcon />
)}
</ListItemIcon>
<ListItemText
primary={answer.task}
primaryTypographyProps={{
sx: {
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
fontSize: 16,
},
}}
/>
</ListItemButton>
</ListItem>
);
})}
</List>
</Stack>
</Stack>
<Stack sx={{ flex: 1, width: "50%", position: "relative" }}>
<AppBar position="static" elevation={0} color="default">
<Toolbar>
<IconButton
color="inherit"
aria-label="Reload"
onClick={() => {
reload();
}}
>
<ReplayIcon />
</IconButton>
</Toolbar>
</AppBar>
{loadingLive && (
<Box
sx={{
position: "absolute",
zIndex: 100,
top: "50%",
left: "50%",
transform: "translate(-50%,-50%)",
}}
>
<CircularProgress />
</Box>
)}
<Box
ref={ref}
component="iframe"
sx={{
width: "100%",
flex: 1,
m: 0,
border: 0,
overflow: "hidden",
visibility: loadingLive ? "hidden" : undefined,
}}
src="/live"
/>
</Stack>
</Stack>
</>
);
}