import { Box, Button, Card, CardContent, CardHeader, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, IconButton, ImageList, ImageListItem, ImageListItemBar, SxProps, TextField, Typography, useTheme, } from "@mui/material" import React, { FC, createRef, useState, useEffect, DragEventHandler, useRef, MouseEventHandler, ChangeEventHandler, } from "react" import ReactMarkdown from "react-markdown" import remarkGfm from "remark-gfm" import remarkMath from "remark-math" import rehypeMathjax from "rehype-mathjax" import rehypeRaw from "rehype-raw" import LoadingButton from "@mui/lab/LoadingButton" import SaveIcon from "@mui/icons-material/Save" import CloseIcon from "@mui/icons-material/Close" import EditIcon from "@mui/icons-material/Edit" import { Theme } from "@mui/material/styles" import { CodeComponent, ReactMarkdownNames, } from "react-markdown/lib/ast-to-react" import HtmlIcon from "@mui/icons-material/Html" import ModeEditIcon from "@mui/icons-material/ModeEdit" import UploadFileIcon from "@mui/icons-material/UploadFile" import { Prism as SyntaxHighlighter } from "react-syntax-highlighter" import { darcula } from "react-syntax-highlighter/dist/esm/styles/prism" import { actionCreator } from "../action" import { useRecoilValue } from "recoil" import { artifactIsAvailable, isFileUploading, useArtifacts } from "../state" const placeholder = `## What is this feature for? Here you can freely take a note in *(GitHub flavored) Markdown format*. In addition, **code blocks with syntax highlights** and **formula** are also supported here, as shown below. ### Code-block with Syntax Highlights \`\`\`python def objective(trial): x = trial.suggest_float("x", -10, 10) y = trial.suggest_float("y", -10, 10) return (x - 5) ** 2 + (y + 5) ** 2 \`\`\` ### Formula $$ L = \\frac{1}{2} \\rho v^2 S C_L $$ ` const CodeBlock: CodeComponent | ReactMarkdownNames = ({ inline, className, children, ...props }) => { const match = /language-(\w+)/.exec(className || "") return !inline && match ? ( {String(children).replace(/\n$/, "")} ) : ( {children} ) } export const TrialNote: FC<{ studyId: number trialId: number latestNote: Note cardSx?: SxProps }> = ({ studyId, trialId, latestNote, cardSx }) => { return ( ) } export const StudyNote: FC<{ studyId: number latestNote: Note cardSx?: SxProps }> = ({ studyId, latestNote, cardSx }) => { return } const useConfirmCloseDialog = ( handleClose: () => void ): [() => void, () => React.ReactNode] => { const theme = useTheme() const [open, setOpen] = useState(false) const zIndex = theme.zIndex.snackbar - 1 const openDialog = () => { setOpen(true) } const renderDialog = () => { return ( Unsaved changes Do you want to save or discard your changes? ) } return [openDialog, renderDialog] } const MarkdownRenderer: FC<{ body: string }> = ({ body }) => ( , }} /> ) const MarkdownEditorModal: FC<{ studyId: number trialId?: number latestNote: Note setEditorUnmount: () => void }> = ({ studyId, trialId, latestNote, setEditorUnmount }) => { const theme = useTheme() const action = actionCreator() const [openConfirmCloseDialog, renderConfirmCloseDialog] = useConfirmCloseDialog(() => { setEditorUnmount() window.onbeforeunload = null }) const [saving, setSaving] = useState(false) const [edited, setEdited] = useState(false) const [curNote, setCurNote] = useState({ version: 0, body: "" }) const textAreaRef = createRef() const notLatest = latestNote.version > curNote.version const artifactEnabled = useRecoilValue(artifactIsAvailable) const [previewMarkdown, setPreviewMarkdown] = useState("") const [preview, setPreview] = useState(false) useEffect(() => { setCurNote(latestNote) }, []) useEffect(() => { if (edited) { window.onbeforeunload = (e) => { e.returnValue = "Are you okay to discard your changes?" } } else { window.onbeforeunload = null } }, [edited]) const handleSave = () => { const nextVersion = curNote.version + 1 const newNote = { version: nextVersion, body: textAreaRef.current ? textAreaRef.current.value : "", } setSaving(true) let actionResponse: Promise if (trialId === undefined) { actionResponse = action.saveStudyNote(studyId, newNote) } else { actionResponse = action.saveTrialNote(studyId, trialId, newNote) } actionResponse .then(() => { setCurNote(newNote) window.onbeforeunload = null setEditorUnmount() }) .finally(() => { setSaving(false) }) } const handleRefresh = () => { if (!textAreaRef.current) { console.log("Unexpectedly, textarea is not found.") return } textAreaRef.current.value = latestNote.body setCurNote(latestNote) window.onbeforeunload = null } const insertTextFromCursorPoint = (text: string) => { if (textAreaRef.current === null) { return } const cursorPosition = textAreaRef.current.selectionStart const currentBody = textAreaRef.current.value textAreaRef.current.value = currentBody.substring(0, cursorPosition) + text + currentBody.substring(cursorPosition, currentBody.length) setEdited(true) } // See https://github.com/iamhosseindhv/notistack/issues/231#issuecomment-825924840 const zIndex = theme.zIndex.snackbar - 2 return ( { setPreview(!preview) setPreviewMarkdown( textAreaRef.current ? textAreaRef.current.value : "" ) }} > {preview ? ( ) : ( )} } title="Markdown Editor" /> { const cur = textAreaRef.current ? textAreaRef.current.value : "" if (edited !== (cur !== curNote.body)) { setEdited(cur !== curNote.body) } }} /> {artifactEnabled && trialId !== undefined && ( )} {notLatest && !saving && ( <> The text you are editing has updated. Do you want to discard your changes and refresh the textarea? )} } variant="contained" disabled={!edited || notLatest} sx={{ marginLeft: theme.spacing(1) }} > Save {renderConfirmCloseDialog()} ) } const ArtifactUploader: FC<{ studyId: number trialId: number insert: (text: string) => void }> = ({ studyId, trialId, insert }) => { const theme = useTheme() const action = actionCreator() const uploading = useRecoilValue(isFileUploading) const artifacts = useArtifacts(studyId, trialId) const [dragOver, setDragOver] = useState(false) const [selectedArtifactId, setSelectedArtifactId] = useState("") const inputRef = useRef(null) const handleClick: MouseEventHandler = (e) => { if (!inputRef || !inputRef.current) { return } inputRef.current.click() } const handleOnChange: ChangeEventHandler = (e) => { const files = e.target.files if (files === null) { return } action.uploadArtifact(studyId, trialId, files[0]) } const handleDrop: DragEventHandler = (e) => { e.stopPropagation() e.preventDefault() const file = e.dataTransfer.files[0] setDragOver(false) action.uploadArtifact(studyId, trialId, file) } const handleDragOver: DragEventHandler = (e) => { e.stopPropagation() e.preventDefault() e.dataTransfer.dropEffect = "copy" setDragOver(true) } const handleDragLeave: DragEventHandler = (e) => { e.stopPropagation() e.preventDefault() e.dataTransfer.dropEffect = "copy" setDragOver(false) } return ( Image } onClick={handleClick} variant="outlined" > Upload {dragOver && ( Upload a New Image Drag your file here. )} {artifacts .filter((a) => a.mimetype.startsWith("image")) .map((a, i) => ( { if (selectedArtifactId === a.artifact_id) { setSelectedArtifactId("") } else { setSelectedArtifactId(a.artifact_id) } }} sx={{ border: selectedArtifactId === a.artifact_id ? `2px solid ${theme.palette.primary.main}` : "none", }} > ))} ) } const NoteBase: FC<{ studyId: number trialId?: number latestNote: Note cardSx?: SxProps }> = ({ studyId, trialId, latestNote, cardSx }) => { const theme = useTheme() const [editorMounted, setEditorMounted] = useState(false) const defaultBody = "" return ( { setEditorMounted(true) }} > {editorMounted && ( { setEditorMounted(false) }} /> )} ) }