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 (
)
}
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)
}}
/>
)}
)
}