Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -49,6 +56,7 @@
"notistack": "^2.0.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.31.2",
"react-dropzone": "^14.2.3",
"react-hook-form": "^7.31.2",
"react-image-crop": "^11.0.10",
Expand Down
141 changes: 141 additions & 0 deletions src/types/Pseudocode/Pseudocode.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
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<BaseResponseAreaProps, 'handleChange' | 'answer' | 'feedback'> & {
handleChange: (val: StudentResponse) => void;
answer?: StudentResponse; // optional, only used for initial load
Comment thread
HongleiGu marked this conversation as resolved.
Outdated
// callAPI: () => void;
Comment thread
HongleiGu marked this conversation as resolved.
Outdated
feedback: EvaluationResult | null;
};

export const PseudocodeInput: React.FC<PseudocodeInputProps> = ({
handleChange,
// callAPI,
feedback,
}) => {
const { classes } = usePseudocodeStyles();

// Internal state fully managed in this component
const [internalAnswer, setInternalAnswer] = useState<StudentResponse>({
pseudocode: '',
time_complexity: '',
space_complexity: '',
explanation: '',
});

const editorRef = useRef<HTMLDivElement | null>(null);
const viewRef = useRef<EditorView | null>(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 (
<div className={classes.root}>
{/* ================= Editor ================= */}
<div className={classes.editorWrapper}>
<div ref={editorRef} className={classes.editor} />
</div>

{/* ================= Complexity Inputs ================= */}
<div className={classes.complexityRow}>
<input
className={classes.field}
value={internalAnswer.time_complexity ?? ''}
placeholder="Time Complexity (e.g. O(n log n))"
onChange={(e) => handleFieldChange('time_complexity', e.target.value)}
/>
<input
className={classes.field}
value={internalAnswer.space_complexity ?? ''}
placeholder="Space Complexity (e.g. O(n))"
onChange={(e) => handleFieldChange('space_complexity', e.target.value)}
/>
</div>

{/* ================= Explanation ================= */}
<textarea
className={classes.textarea}
value={internalAnswer.explanation ?? ''}
placeholder="Explain your reasoning (optional)"
onChange={(e) => handleFieldChange('explanation', e.target.value)}
/>

{/* ================= Action =================
<button onClick={callAPI}>
Check Answer
</button> */}

{/* ================= Feedback ================= */}
{feedback && (
<div className={classes.feedbackPanel}>
<PseudocodeFeedbackPanel result={feedback} />
</div>
)}
</div>
);
};
Loading