diff --git a/externals/styles/fonts.tsx b/externals/styles/fonts.tsx index a50408e..4cd056f 100644 --- a/externals/styles/fonts.tsx +++ b/externals/styles/fonts.tsx @@ -1,80 +1,102 @@ -import { Fira_Sans, Fira_Mono, Lato, Roboto } from 'next/font/google' +// import { Fira_Sans, Fira_Mono, Lato, Roboto } from 'next/font/google' -export const roboto = Roboto({ - subsets: ['latin'], - weight: ['400', '700'], - style: ['normal', 'italic'], - fallback: [ - '-apple-system', - 'BlinkMacSystemFont', - 'Segoe UI', - 'Roboto', - 'Helvetica Neue', - 'Arial', - 'sans-serif', - 'Apple Color Emoji', - 'Segoe UI Emoji', - 'Segoe UI Symbol', - ], - variable: '--font-roboto', - preload: false, -}) +// export const roboto = Roboto({ +// subsets: ['latin'], +// weight: ['400', '700'], +// style: ['normal', 'italic'], +// fallback: [ +// '-apple-system', +// 'BlinkMacSystemFont', +// 'Segoe UI', +// 'Roboto', +// 'Helvetica Neue', +// 'Arial', +// 'sans-serif', +// 'Apple Color Emoji', +// 'Segoe UI Emoji', +// 'Segoe UI Symbol', +// ], +// variable: '--font-roboto', +// preload: false, +// }) -export const firaSans = Fira_Sans({ - subsets: ['latin'], - weight: ['300', '400', '500', '600', '700'], - style: ['normal', 'italic'], - fallback: [ - '-apple-system', - 'BlinkMacSystemFont', - 'Segoe UI', - 'Roboto', - 'Helvetica Neue', - 'Arial', - 'sans-serif', - 'Apple Color Emoji', - 'Segoe UI Emoji', - 'Segoe UI Symbol', - ], - variable: '--font-fira-sans', - preload: false, -}) +// export const firaSans = Fira_Sans({ +// subsets: ['latin'], +// weight: ['300', '400', '500', '600', '700'], +// style: ['normal', 'italic'], +// fallback: [ +// '-apple-system', +// 'BlinkMacSystemFont', +// 'Segoe UI', +// 'Roboto', +// 'Helvetica Neue', +// 'Arial', +// 'sans-serif', +// 'Apple Color Emoji', +// 'Segoe UI Emoji', +// 'Segoe UI Symbol', +// ], +// variable: '--font-fira-sans', +// preload: false, +// }) -export const firaMono = Fira_Mono({ - subsets: ['latin'], - weight: ['400'], - fallback: [ - '-apple-system', - 'BlinkMacSystemFont', - 'Segoe UI', - 'Roboto', - 'Helvetica Neue', - 'Arial', - 'sans-serif', - 'Apple Color Emoji', - 'Segoe UI Emoji', - 'Segoe UI Symbol', - ], - variable: '--font-fira-mono', - preload: false, -}) +// export const firaMono = Fira_Mono({ +// subsets: ['latin'], +// weight: ['400'], +// fallback: [ +// '-apple-system', +// 'BlinkMacSystemFont', +// 'Segoe UI', +// 'Roboto', +// 'Helvetica Neue', +// 'Arial', +// 'sans-serif', +// 'Apple Color Emoji', +// 'Segoe UI Emoji', +// 'Segoe UI Symbol', +// ], +// variable: '--font-fira-mono', +// preload: false, +// }) -export const lato = Lato({ - subsets: ['latin'], - weight: ['400'], - style: ['normal', 'italic'], - fallback: [ - '-apple-system', - 'BlinkMacSystemFont', - 'Segoe UI', - 'Roboto', - 'Helvetica Neue', - 'Arial', - 'sans-serif', - 'Apple Color Emoji', - 'Segoe UI Emoji', - 'Segoe UI Symbol', - ], - variable: '--font-lato', - preload: false, -}) +// export const lato = Lato({ +// subsets: ['latin'], +// weight: ['400'], +// style: ['normal', 'italic'], +// fallback: [ +// '-apple-system', +// 'BlinkMacSystemFont', +// 'Segoe UI', +// 'Roboto', +// 'Helvetica Neue', +// 'Arial', +// 'sans-serif', +// 'Apple Color Emoji', +// 'Segoe UI Emoji', +// 'Segoe UI Symbol', +// ], +// variable: '--font-lato', +// preload: false, +// }) + +// please see the sandbox issue #2 for this + +export const firaSans = { + className: 'font-fira-sans', + style: { fontFamily: 'Fira Sans, sans-serif' }, +} + +export const firaMono = { + className: 'font-fira-mono', + style: { fontFamily: 'Fira Mono, monospace' }, +} + +export const lato = { + className: 'font-lato', + style: { fontFamily: 'Lato, sans-serif' }, +} + +export const roboto = { + className: 'font-roboto', + style: { fontFamily: 'Roboto, sans-serif' }, +} \ No newline at end of file diff --git a/package.json b/package.json index b7a8bf5..d8364ec 100644 --- a/package.json +++ b/package.json @@ -17,10 +17,16 @@ "dev": "run-p build:watch preview" }, "dependencies": { + "@codemirror/basic-setup": "^0.20.0", + "@codemirror/lang-javascript": "^6.2.4", + "@codemirror/language": "^6.12.1", + "@codemirror/state": "^6.5.4", + "@codemirror/view": "^6.39.12", "@date-io/date-fns": "^2.13.2", "@emotion/react": "^11.4.1", "@emotion/server": "^11.4.0", "@emotion/styled": "^11.3.0", + "@lezer/highlight": "^1.2.3", "@monaco-editor/react": "^4.6.0", "@mui/icons-material": "^5.15.20", "@mui/lab": "^5.0.0-alpha.100", @@ -35,6 +41,7 @@ "@nivo/core": "^0.88.0", "@nivo/line": "^0.88.0", "@nivo/pie": "^0.88.0", + "codemirror": "^6.0.2", "axios": "^1.7.2", "date-fns": "^2.28.0", "firebase": "^11.0.1", @@ -49,8 +56,8 @@ "notistack": "^2.0.3", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-dropzone": "^14.2.3", "react-hook-form": "^7.31.2", + "react-dropzone": "^14.2.3", "react-image-crop": "^11.0.10", "react-katex": "^3.0.1", "react-query": "^3.39.0", diff --git a/src/sandbox-component.tsx b/src/sandbox-component.tsx index ab01af5..4ecbdf3 100644 --- a/src/sandbox-component.tsx +++ b/src/sandbox-component.tsx @@ -1,4 +1,6 @@ import { ThemeProvider } from '@styles/minimal/theme-provider' + +import { PseudocodeResponseAreaTub } from './types/Pseudocode' import { SandboxResponseAreaTub } from './types/Sandbox/index' function ResponseAreaInputWrapper({ children }: { children: React.ReactNode }) { @@ -6,7 +8,7 @@ function ResponseAreaInputWrapper({ children }: { children: React.ReactNode }) { } // wrap the components with the necessary providers; only in the sandbox -class WrappedSandboxResponseAreaTub extends SandboxResponseAreaTub { +class WrappedSandboxResponseAreaTub extends PseudocodeResponseAreaTub { constructor() { super() diff --git a/src/types/Essay/EssayWizard.component.tsx b/src/types/Essay/EssayWizard.component.tsx index 02cd163..c536711 100644 --- a/src/types/Essay/EssayWizard.component.tsx +++ b/src/types/Essay/EssayWizard.component.tsx @@ -37,7 +37,6 @@ export const EssayWizard: React.FC = props => { onChange, } = props const { classes, cx } = useStyles() - console.log('answer', answer) const updateAnswer = (newAnswer: string) => { onChange({ diff --git a/src/types/Pseudocode/Pseudocode.component.tsx b/src/types/Pseudocode/Pseudocode.component.tsx new file mode 100644 index 0000000..bd7c2c8 --- /dev/null +++ b/src/types/Pseudocode/Pseudocode.component.tsx @@ -0,0 +1,136 @@ +import { foldGutter, foldService, syntaxHighlighting } from '@codemirror/language'; +import { EditorState } from '@codemirror/state'; +import { placeholder } from '@codemirror/view'; +import { EditorView, basicSetup } from 'codemirror'; +import { useEffect, useRef, useState } from 'react'; + +import { BaseResponseAreaProps } from '../base-props.type'; + +import { PseudocodeFeedbackPanel } from './components/PseudocodeFeedbackPanel'; +import { autoIndentAfterColon } from './plugins/autoIndent'; +import { pseudocodeFoldFunc } from './plugins/fold'; +import { pseudocodeHighlightStyle } from './plugins/highlight'; +import { pseudocodeLanguage } from './plugins/language'; +import { pseudocodeTheme } from './plugins/pseudocode.theme'; +import { StudentResponse } from './types/input'; +// import { defaultStudentResponse } from './utils/consts'; +import { EvaluationResult } from './types/output'; +import { usePseudocodeStyles } from './utils/styles'; + +type PseudocodeInputProps = Omit & { + handleChange: (val: StudentResponse) => void; + feedback: EvaluationResult | null; + answer: StudentResponse +}; + +export const PseudocodeInput: React.FC = ({ + handleChange, + feedback, + answer +}) => { + console.log(answer) + const { classes } = usePseudocodeStyles(); + + // Internal state fully managed in this component + const [internalAnswer, setInternalAnswer] = useState(answer); + + const editorRef = useRef(null); + const viewRef = useRef(null); + + // Initialize CodeMirror once + useEffect(() => { + if (!editorRef.current) return; + + const state = EditorState.create({ + doc: internalAnswer.pseudocode, + extensions: [ + foldGutter(), + foldService.of(pseudocodeFoldFunc), + autoIndentAfterColon, + basicSetup, + pseudocodeLanguage, + syntaxHighlighting(pseudocodeHighlightStyle), + pseudocodeTheme, + placeholder('Write your pseudocode here...'), + EditorView.updateListener.of((update) => { + if (!update.docChanged) return; + const newCode = update.state.doc.toString(); + setInternalAnswer((prev) => { + const updated = { ...prev, pseudocode: newCode }; + handleChange(updated); // notify parent immediately + return updated; + }); + }), + EditorView.theme({ + '&': { height: '100%' }, + '.cm-scroller': { overflow: 'auto' }, + }), + ], + }); + + const view = new EditorView({ + state, + parent: editorRef.current, + }); + + viewRef.current = view; + return () => { + view.destroy(); + viewRef.current = null; + }; + }, []); // only runs once + + // Report change for complexity/explanation fields + const handleFieldChange = (field: keyof StudentResponse, value: string) => { + setInternalAnswer((prev) => { + const updated = { ...prev, [field]: value }; + handleChange(updated); // immediate update to parent + return updated; + }); + }; + + return ( +
+ {/* ================= Editor ================= */} +
+
+
+ + {/* ================= Complexity Inputs ================= */} +
+ handleFieldChange('time_complexity', e.target.value)} + /> + handleFieldChange('space_complexity', e.target.value)} + /> +
+ + {/* ================= Explanation ================= */} +