Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: store handwriting #21

Merged
merged 12 commits into from
Jul 19, 2024
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Excalidraw, MainMenu } from '@excalidraw/excalidraw'
import { ClipboardData } from '@excalidraw/excalidraw/types/clipboard'
import { ExcalidrawElement } from '@excalidraw/excalidraw/types/element/types'
import { AppState, ExcalidrawImperativeAPI } from '@excalidraw/excalidraw/types/types'
import { Box } from '@radix-ui/themes'
import React, {
Expand All @@ -13,6 +14,7 @@ import React, {

import { ConfirmDialog } from '../../../../ConfirmDialog'
import useLiveUpdates from './live-updates.hook'
import { HandwritingAnswer } from './types'

const stopEvent = (e: SyntheticEvent | Event) => {
e.preventDefault()
Expand All @@ -22,6 +24,10 @@ const stopEvent = (e: SyntheticEvent | Event) => {
interface CanvasProps {
username: string
onAnswerChange: (value: string) => void
initialData: {
elements?: readonly ExcalidrawElement[]
appState?: AppState
}
}

// Excalidraw keyboard shortcuts we allow in the canvas:
Expand All @@ -44,11 +50,25 @@ const ALLOWED_TOOL_SHORTCUTS = [
'Digit7', // pen
]

const Canvas: React.FC<CanvasProps> = ({ username, onAnswerChange }) => {
const { updateStrokes } = useLiveUpdates(username, onAnswerChange)
const Canvas: React.FC<CanvasProps> = ({ username, onAnswerChange, initialData }) => {
const [excalidrawAPI, setExcalidrawAPI] = useState<ExcalidrawImperativeAPI | null>(null)
const [clearDialogOpen, setClearDialogOpen] = useState(false)

const updateHandwriting = useCallback(
(latex: string) => {
const result: HandwritingAnswer = {
latex,
excalidraw: {
elements: excalidrawAPI?.getSceneElements() ?? [],
appState: excalidrawAPI?.getAppState(),
},
}
onAnswerChange(JSON.stringify(result))
},
[excalidrawAPI, onAnswerChange]
)
const { updateStrokes } = useLiveUpdates(username, updateHandwriting)

const clearCanvas = useCallback(() => {
updateStrokes({ elements: [] })
excalidrawAPI?.updateScene({ elements: [] })
Expand Down Expand Up @@ -128,7 +148,10 @@ const Canvas: React.FC<CanvasProps> = ({ username, onAnswerChange }) => {
UIOptions={{ tools: { image: false } }}
gridModeEnabled
excalidrawAPI={setExcalidrawAPI}
// initialData={excalidrawData}
initialData={{
...initialData,
appState: { ...initialData.appState, collaborators: new Map() },
}}
onPaste={pasteHandler}
>
<MainMenu>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,33 @@ import React from 'react'
import Markdown from '../../../../Markdown'
import Canvas from './Canvas'
import './handwritingEditor.css'
import { HandwritingAnswer } from './types'

interface HandwritingEditorProps {
username: string
latex: string
answer?: HandwritingAnswer
onAnswerChange: (value: string) => void
}

const HandwritingEditor: React.FC<HandwritingEditorProps> = ({
username,
latex,
answer,
onAnswerChange,
}) => {
return (
<Flex direction="column" flexGrow="1" align="center" gap="3">
<Card className="mathjax-card">
<Box p="3">
<MathJax>{`\\( ${latex} \\)`}</MathJax>
<MathJax>{`\\( ${answer?.latex} \\)`}</MathJax>
Gum-Joe marked this conversation as resolved.
Show resolved Hide resolved
</Box>
</Card>
<Card className="excalidraw-canvas-card">
<Canvas username={username} onAnswerChange={onAnswerChange} />
<Canvas
username={username}
onAnswerChange={onAnswerChange}
// @ts-expect-error: this is not just initialData
initialData={answer?.excalidraw ?? {}}
Gum-Joe marked this conversation as resolved.
Show resolved Hide resolved
/>
</Card>
</Flex>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,38 @@
import { Excalidraw } from '@excalidraw/excalidraw'
import {
ExcalidrawImperativeAPI,
ExcalidrawInitialDataState,
} from '@excalidraw/excalidraw/types/types'
import { ExcalidrawElement } from '@excalidraw/excalidraw/types/element/types'
import { ExcalidrawImperativeAPI } from '@excalidraw/excalidraw/types/types'
import { Box, Card } from '@radix-ui/themes'
import React, { useEffect, useState } from 'react'

interface ViewOnlyCanvasProps {
initialData: ExcalidrawInitialDataState
initialData: readonly ExcalidrawElement[]
}

export const ViewOnlyCanvas: React.FC<ViewOnlyCanvasProps> = ({ initialData }) => {
const [excalidrawAPI, setExcalidrawAPI] = useState<ExcalidrawImperativeAPI | null>(null)

useEffect(() => {
if (excalidrawAPI && initialData) {
excalidrawAPI.updateScene({
elements: initialData,
})
}
setTimeout(() => {
excalidrawAPI?.scrollToContent(undefined, {
fitToContent: true,
})
})
}, [excalidrawAPI])
}, [excalidrawAPI, initialData])

return initialData && initialData.elements?.length ? (
return (
<Card>
<Box className="excalidraw-view-container excalidraw-box">
<Excalidraw excalidrawAPI={setExcalidrawAPI} viewModeEnabled initialData={initialData} />
<Excalidraw
excalidrawAPI={setExcalidrawAPI}
viewModeEnabled
initialData={{ elements: initialData }}
/>
</Box>
</Card>
) : null
)
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Pencil2Icon } from '@radix-ui/react-icons'
import { Button, Card, Dialog, Flex } from '@radix-ui/themes'
import { MathJax } from 'better-react-mathjax'
import { isEmpty } from 'lodash'
import { FC } from 'react'
import { useParams } from 'react-router-dom'

Expand All @@ -10,8 +11,9 @@ import { TaskBaseProps } from '../../types'
import HandwritingEditor from './HandwritingEditor'
import { ViewOnlyCanvas } from './ViewOnlyCanvas'
import './index.scss'
import { HandwritingAnswer } from './types'

export interface HandwritingTaskProps extends TaskBaseProps<string> {
export interface HandwritingTaskProps extends TaskBaseProps<HandwritingAnswer> {
type: TaskType.PROCESSED_HANDWRITING
}

Expand All @@ -25,7 +27,9 @@ export const HandwritingTask: FC<HandwritingTaskProps> = ({
return (
<Dialog.Root>
<Dialog.Trigger>
<ViewOnlyCanvas initialData={{}} />
{!isEmpty(answer?.excalidraw?.elements) && (
<ViewOnlyCanvas initialData={answer!.excalidraw.elements} />
)}
</Dialog.Trigger>
<Flex gap="3" align="center">
<Card className="latex-preview">
Expand All @@ -42,11 +46,7 @@ export const HandwritingTask: FC<HandwritingTaskProps> = ({

<Dialog.Content className="excalidraw-dialog-content">
<Flex direction="column" height="100%" gap="3">
<HandwritingEditor
latex={answer ?? ''}
onAnswerChange={onAnswerUpdate}
username={username}
/>
<HandwritingEditor answer={answer} onAnswerChange={onAnswerUpdate} username={username} />
<Flex justify="end">
<Dialog.Close>
<Button>Save LaTeX</Button>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { ExcalidrawElement } from '@excalidraw/excalidraw/types/element/types'
import { AppState } from '@excalidraw/excalidraw/types/types'

export interface HandwritingAnswer {
excalidraw: {
alexanderbira marked this conversation as resolved.
Show resolved Hide resolved
elements: readonly ExcalidrawElement[]
appState?: Omit<AppState, 'offsetTop' | 'offsetLeft' | 'width' | 'height'>
Gum-Joe marked this conversation as resolved.
Show resolved Hide resolved
}
latex: string
}
2 changes: 2 additions & 0 deletions src/utils/answers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export function parseAnswer(answer: string, targetTaskType: TaskType) {
return answer === '' ? answer : Number(answer)
case TaskType.MULTIPLE_CHOICE_SELECT_SEVERAL:
return answer === '' ? [] : answer.split(',')
case TaskType.PROCESSED_HANDWRITING:
return answer === '' ? { excalidraw: null, latex: '' } : JSON.parse(answer)
default:
return answer
}
Expand Down