Skip to content

Commit 9d77218

Browse files
committed
feat: add MCQ interface
- Integrate new message type: MCQQuestion - Add UI component for the MCQ Panel - Add Markdown rendering
1 parent d06f420 commit 9d77218

File tree

3 files changed

+243
-12
lines changed

3 files changed

+243
-12
lines changed

src/commands/showPanel.tsx

Lines changed: 173 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import { FRONTEND_ELEMENT_ID } from "../constants";
1515
import { client } from "../extension";
1616
import _ from "lodash";
1717

18+
let mcqPanel: vscode.WebviewPanel | null = null;
19+
1820
let panel: vscode.WebviewPanel | null = null;
1921
// This needs to be a reference to active
2022
// TODO: Fix this ugly code!
@@ -42,11 +44,43 @@ async function handleMessage(
4244
case MessageTypeNames.ExtensionPing:
4345
sendToFrontend(panel, Messages.ExtensionPong(null));
4446
break;
45-
case MessageTypeNames.NewEditor:
46-
console.log(message.questionType + " questionType \n");
47-
if (message.questionType == "mcq") {
48-
break;
47+
case MessageTypeNames.MCQQuestion:
48+
{
49+
if (mcqPanel === null) {
50+
mcqPanel = vscode.window.createWebviewPanel(
51+
"mcq-question-panel",
52+
`Question ${message.questionId + 1}`,
53+
vscode.ViewColumn.One,
54+
{ enableScripts: true, retainContextWhenHidden: true },
55+
);
56+
mcqPanel.onDidDispose(() => {
57+
mcqPanel = null;
58+
});
59+
}
60+
mcqPanel.title = `Question ${message.questionId + 1}`;
61+
mcqPanel.iconPath = vscode.Uri.joinPath(
62+
context.extensionUri,
63+
"assets",
64+
"icon.png",
65+
);
66+
67+
// Cast message to ensure properties exist
68+
const mcqMsg = message as any;
69+
mcqPanel.webview.html = getMcqHtml(
70+
mcqPanel.webview,
71+
mcqMsg.question,
72+
mcqMsg.options,
73+
mcqMsg.questionId,
74+
);
75+
mcqPanel.reveal(vscode.ViewColumn.One);
4976
}
77+
break;
78+
79+
case MessageTypeNames.NewEditor:
80+
// console.log(message.questionType + " questionType \n");
81+
// if (message.questionType == "mcq") {
82+
// break;
83+
// }
5084
activeEditor = await Editor.create(
5185
message.workspaceLocation,
5286
message.assessmentName,
@@ -73,12 +107,7 @@ async function handleMessage(
73107
console.log(
74108
`EXTENSION: NewEditor: activeEditor set to ${activeEditor.assessmentName}_${activeEditor.questionId}`,
75109
);
76-
if (activeEditor) {
77-
console.log("activeEditor keys and values:");
78-
Object.entries(activeEditor).forEach(([key, value]) => {
79-
console.log(`${key}:`, value);
80-
});
81-
}
110+
82111
activeEditor.onChange((editor) => {
83112
const workspaceLocation = editor.workspaceLocation;
84113
const code = editor.getText();
@@ -172,6 +201,140 @@ export async function showPanel(context: vscode.ExtensionContext) {
172201
}
173202

174203
// TODO: Move this to a util file
204+
function getMcqHtml(
205+
_webview: vscode.Webview,
206+
question: string,
207+
options: string[],
208+
questionId: string,
209+
): string {
210+
return `<!DOCTYPE html>
211+
<html lang="en">
212+
<head>
213+
<meta charset="UTF-8" />
214+
<meta name="viewport" content="width=device-width, initial-scale=1" />
215+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-eval' 'unsafe-inline' https://unpkg.com; style-src 'unsafe-inline' https://unpkg.com;" />
216+
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
217+
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
218+
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
219+
<script src="https://unpkg.com/marked@4.0.0/marked.min.js"></script>
220+
<script>window.marked.setOptions({ breaks: true });</script>
221+
<style>
222+
.mcq-option {
223+
color: black !important;
224+
}
225+
.mcq-option p {
226+
margin: 0;
227+
display: inline;
228+
}
229+
</style>
230+
</head>
231+
<body>
232+
<div id="root"></div>
233+
<script type="text/babel" data-type="module">
234+
const { useState } = React;
235+
236+
function McqPanel({ question, options, questionId }) {
237+
const [selected, setSelected] = useState(null);
238+
239+
const handleSubmit = (e) => {
240+
e.preventDefault();
241+
if (selected === null) return;
242+
const vscode = acquireVsCodeApi();
243+
vscode.postMessage({ type: 'answer', answer: selected });
244+
};
245+
246+
return (
247+
<div style={{
248+
padding: '1rem',
249+
fontFamily: 'sans-serif',
250+
maxWidth: '800px',
251+
margin: '0 auto'
252+
}}>
253+
<h3>Question {parseInt(questionId) + 1}</h3>
254+
<div dangerouslySetInnerHTML={{ __html: window.marked.parse(question) }} />
255+
<form onSubmit={handleSubmit}>
256+
<ul style={{
257+
listStyle: 'none',
258+
padding: 0,
259+
margin: '1rem 0 1.5rem 0'
260+
}}>
261+
{options.map((option, index) => (
262+
<li
263+
key={index}
264+
style={{
265+
margin: '0.5rem 0',
266+
padding: '0.75rem',
267+
border: '1px solid #e1e4e8',
268+
borderRadius: '6px',
269+
backgroundColor: selected === index ? '#f6f8fa' : 'white',
270+
cursor: 'pointer',
271+
transition: 'background-color 0.2s'
272+
}}
273+
onClick={() => setSelected(index)}
274+
>
275+
<label style={{
276+
display: 'flex',
277+
alignItems: 'center',
278+
cursor: 'pointer',
279+
margin: 0
280+
}}>
281+
<input
282+
type="radio"
283+
name="mcq-option"
284+
checked={selected === index}
285+
onChange={() => setSelected(index)}
286+
style={{
287+
marginRight: '0.75rem',
288+
width: '1.25rem',
289+
height: '1.25rem',
290+
cursor: 'pointer'
291+
}}
292+
/>
293+
<span
294+
className="mcq-option"
295+
dangerouslySetInnerHTML={{ __html: window.marked.parse(option) }}
296+
/>
297+
</label>
298+
</li>
299+
))}
300+
</ul>
301+
<button
302+
type="submit"
303+
disabled={selected === null}
304+
style={{
305+
padding: '0.5rem 1.5rem',
306+
backgroundColor: selected !== null ? '#2ea043' : '#94d3a2',
307+
color: 'white',
308+
border: 'none',
309+
borderRadius: '6px',
310+
cursor: selected !== null ? 'pointer' : 'not-allowed',
311+
fontSize: '1rem',
312+
fontWeight: '500',
313+
transition: 'background-color 0.2s',
314+
opacity: selected !== null ? 1 : 0.7
315+
}}
316+
>
317+
Submit Answer
318+
</button>
319+
</form>
320+
</div>
321+
);
322+
}
323+
324+
// Render the component
325+
const root = ReactDOM.createRoot(document.getElementById('root'));
326+
root.render(
327+
<McqPanel
328+
question="${question.replace(/"/g, "&quot;")}"
329+
questionId="${questionId}"
330+
options={${JSON.stringify(options)}}
331+
/>
332+
);
333+
</script>
334+
</body>
335+
</html>`;
336+
}
337+
175338
export async function sendToFrontendWrapped(message: MessageType) {
176339
if (!panel) {
177340
console.error("ERROR: panel is not set");

src/utils/messages.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,13 @@ const Messages = createMessages({
2222
workspaceLocation: VscWorkspaceLocation,
2323
assessmentName: string,
2424
questionId: number,
25-
questionType: string | null,
2625
chapter: number,
2726
prepend: string,
2827
initialCode: string,
2928
) => ({
3029
workspaceLocation,
3130
assessmentName,
3231
questionId,
33-
questionType,
3432
chapter,
3533
prepend,
3634
initialCode,
@@ -42,6 +40,23 @@ const Messages = createMessages({
4240
EvalEditor: (workspaceLocation: VscWorkspaceLocation) => ({
4341
workspaceLocation: workspaceLocation,
4442
}),
43+
MCQQuestion: (
44+
workspaceLocation: VscWorkspaceLocation,
45+
assessmentName: string,
46+
questionId: number,
47+
chapter: number,
48+
question: string,
49+
options: string[],
50+
correctOption: number,
51+
) => ({
52+
workspaceLocation,
53+
assessmentName,
54+
questionId,
55+
chapter,
56+
question,
57+
options,
58+
correctOption,
59+
}),
4560
});
4661

4762
export default Messages;

src/webview/components/McqPanel.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React, { useState } from "react";
2+
import Messages from "../../utils/messages";
3+
4+
export interface McqData {
5+
assessmentName: string;
6+
questionId: number;
7+
question: string;
8+
choices: string[];
9+
}
10+
11+
interface McqPanelProps {
12+
data: McqData;
13+
onAnswer: (choiceIndex: number) => void;
14+
}
15+
16+
const McqPanel: React.FC<McqPanelProps> = ({ data, onAnswer }) => {
17+
const [selected, setSelected] = useState<number | null>(null);
18+
19+
return (
20+
<div style={{ padding: "1rem" }}>
21+
<h3>{data.question}</h3>
22+
<ul style={{ listStyle: "none", paddingLeft: 0 }}>
23+
{data.choices.map((c, idx) => (
24+
<li key={idx} style={{ marginBottom: "0.5rem" }}>
25+
<label style={{ cursor: "pointer" }}>
26+
<input
27+
type="radio"
28+
name="mcq-choice"
29+
value={idx}
30+
checked={selected === idx}
31+
onChange={() => setSelected(idx)}
32+
style={{ marginRight: "0.5rem" }}
33+
/>
34+
{c}
35+
</label>
36+
</li>
37+
))}
38+
</ul>
39+
<button
40+
disabled={selected === null}
41+
onClick={() => {
42+
if (selected !== null) {
43+
onAnswer(selected);
44+
}
45+
}}
46+
>
47+
Submit
48+
</button>
49+
</div>
50+
);
51+
};
52+
53+
export default McqPanel;

0 commit comments

Comments
 (0)