import React, { useState, useRef, useCallback, useEffect } from 'react';
import Editor from '@monaco-editor/react';
import { AI_SUGGESTIONS_BY_PROBLEM, DEFAULT_SUGGESTIONS, LANGUAGE_MAP } from '../constants';
/**
* @fileoverview Problem page component for the AutoSuggestion Quiz application.
* @module ProblemPage
*/
/**
* @typedef {Object} Example
* @property {string} input - The example input value.
* @property {string} output - The expected output for the given input.
* @property {string} [explanation] - Optional explanation of why the output is correct.
*/
/**
* @typedef {Object} Problem
* @property {string} id - Unique identifier used to look up AI suggestions.
* @property {string} title - Display title of the problem.
* @property {string} description - Full problem description shown to the user.
* @property {Object.<string, string>} starterCode - Map of language key to starter code string (e.g. `{ python: '...', javascript: '...' }`).
* @property {Example[]} examples - List of input/output examples shown in the problem panel.
*/
/**
* @typedef {Object} SuggestionLogEntry
* @property {string} time - Locale time string of when the suggestion was accepted.
* @property {'accepted'} action - The action taken on the suggestion.
* @property {string} label - The label of the accepted suggestion.
*/
/**
* A full-page coding environment that presents a problem description alongside
* a Monaco editor. Features include:
* - Per-problem AI autocompletion suggestions that trigger after 2 seconds of idle typing.
* - In-browser Python execution via Pyodide.
* - Mock execution for non-Python languages.
* - A suggestion log that records every accepted AI suggestion.
*
* @component
* @param {Object} props
* @param {Problem} props.problem - The problem data to display and solve.
* @param {function(): void} props.onBack - Callback invoked when the user navigates back or submits.
* @returns {React.ReactElement} The rendered problem page.
*
* @example
* <ProblemPage problem={selectedProblem} onBack={() => setPage('home')} />
*/
function ProblemPage({ problem, onBack }) {
/** @type {[string, function(string): void]} Currently selected language key (e.g. `'python'`). */
const [language, setLanguage] = useState('python');
/** @type {[string, function(string): void]} Current contents of the code editor. */
const [code, setCode] = useState(problem.starterCode.python);
/** @type {[string, function(string): void]} Output text displayed in the output tab. */
const [output, setOutput] = useState('');
/** @type {[boolean, function(boolean): void]} Whether code is currently being executed. */
const [isRunning, setIsRunning] = useState(false);
/** @type {['output'|'log', function(string): void]} Which bottom panel tab is active. */
const [activeTab, setActiveTab] = useState('output');
/** @type {[SuggestionLogEntry[], function(SuggestionLogEntry[]): void]} Log of accepted AI suggestions. */
const [suggestionLog, setSuggestionLog] = useState([]);
/** @type {React.MutableRefObject<import('monaco-editor').editor.IStandaloneCodeEditor|null>} Reference to the Monaco editor instance. */
const editorRef = useRef(null);
/** @type {React.MutableRefObject<typeof import('monaco-editor')|null>} Reference to the Monaco namespace. */
const monacoRef = useRef(null);
/** @type {React.MutableRefObject<ReturnType<typeof setTimeout>|null>} Timer ref for the idle-trigger debounce. */
const idleTimerRef = useRef(null);
/** @type {React.MutableRefObject<{dispose: function(): void}|null>} Disposable returned by `registerCompletionItemProvider`. */
const completionProviderRef = useRef(null);
/** @type {[Object|null, function(Object|null): void]} The loaded Pyodide instance, or null if not yet ready. */
const [pyodide, setPyodide] = useState(null);
/** @type {[boolean, function(boolean): void]} Whether Pyodide is still initializing. */
const [pyodideLoading, setPyodideLoading] = useState(true);
/**
* Registers (or re-registers) the AI completion item provider for the given language.
* Disposes of any previously registered provider before creating a new one.
* Suggestions are sourced from {@link AI_SUGGESTIONS_BY_PROBLEM} keyed by problem ID,
* falling back to {@link DEFAULT_SUGGESTIONS}.
*
* @function
* @param {typeof import('monaco-editor')} monaco - The Monaco namespace.
* @param {string} lang - The Monaco language identifier (e.g. `'python'`, `'javascript'`).
* @returns {void}
*/
const registerCompletionProvider = useCallback(
(monaco, lang) => {
if (completionProviderRef.current) {
completionProviderRef.current.dispose();
completionProviderRef.current = null;
}
const suggestions =
AI_SUGGESTIONS_BY_PROBLEM[problem.id] || DEFAULT_SUGGESTIONS;
completionProviderRef.current =
monaco.languages.registerCompletionItemProvider(lang, {
triggerCharacters: [],
/**
* Provides AI completion items at the current cursor position.
*
* @param {import('monaco-editor').editor.ITextModel} model - The current text model.
* @param {import('monaco-editor').Position} position - The current cursor position.
* @returns {{ suggestions: import('monaco-editor').languages.CompletionItem[] }}
*/
provideCompletionItems(model, position) {
const word = model.getWordUntilPosition(position);
const range = {
startLineNumber: position.lineNumber,
startColumn: word.startColumn,
endLineNumber: position.lineNumber,
endColumn: word.endColumn,
};
return {
suggestions: suggestions.map((s, idx) => ({
label: s.label,
kind: monaco.languages.CompletionItemKind.Snippet,
detail: s.detail || 'AI Suggestion',
documentation: {
value: '```' + lang + '\n' + s.insertText + '\n```',
},
insertText: s.insertText,
insertTextRules:
monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,
range,
sortText: `0${idx}`,
command: {
id: 'ai-suggestion-accepted',
title: 'AI Suggestion Accepted',
arguments: [s.label],
},
})),
};
},
});
},
[problem.id]
);
/**
* Callback fired when the Monaco editor finishes mounting.
* Attaches the AI suggestion acceptance action, an optional paste interceptor,
* an idle-trigger for autocomplete, and registers the initial completion provider.
*
* @function
* @param {import('monaco-editor').editor.IStandaloneCodeEditor} editor - The mounted editor instance.
* @param {typeof import('monaco-editor')} monaco - The Monaco namespace.
* @returns {void}
*/
const handleEditorDidMount = useCallback(
(editor, monaco) => {
editorRef.current = editor;
monacoRef.current = monaco;
editor.addCommand(0, () => {}, '');
/**
* Editor action that records an accepted AI suggestion to the suggestion log.
* Triggered via the `'ai-suggestion-accepted'` command attached to each completion item.
*/
editor.addAction({
id: 'ai-suggestion-accepted',
label: 'AI Suggestion Accepted',
run: (_ed, label) => {
setSuggestionLog((prev) => [
...prev,
{
time: new Date().toLocaleTimeString(),
action: 'accepted',
label: label || 'unknown',
},
]);
},
});
editor.onKeyDown((e) => {
if ((e.ctrlKey || e.metaKey) && e.code === 'KeyV') {
// Uncomment to enable anti-paste:
// e.preventDefault();
// e.stopPropagation();
}
});
/**
* After each content change, reset the idle timer.
* When the editor has been idle for 2 seconds and still has focus,
* the suggestion dropdown is triggered programmatically.
*/
editor.onDidChangeModelContent(() => {
if (idleTimerRef.current) {
clearTimeout(idleTimerRef.current);
}
idleTimerRef.current = setTimeout(() => {
if (editor.hasTextFocus()) {
editor.trigger('ai-idle', 'editor.action.triggerSuggest', {});
}
}, 2000);
});
registerCompletionProvider(monaco, LANGUAGE_MAP[language]);
},
[registerCompletionProvider, language]
);
/**
* Cleanup effect: clears the idle timer and disposes the completion provider
* when the component unmounts to prevent memory leaks.
*/
useEffect(() => {
return () => {
if (idleTimerRef.current) clearTimeout(idleTimerRef.current);
if (completionProviderRef.current) completionProviderRef.current.dispose();
};
}, []);
/**
* Initialization effect: dynamically loads the Pyodide script and initializes
* the Python runtime on mount. Handles three scenarios:
* 1. `window.loadPyodide` is already available (e.g. cached).
* 2. The script tag is already in the DOM but still loading.
* 3. The script needs to be injected fresh into `document.head`.
*
* On failure, sets an error message in the output panel.
*
* @see {@link https://pyodide.org/en/stable/}
*/
useEffect(() => {
const initPyodide = async () => {
try {
setPyodideLoading(true);
if (window.loadPyodide) {
const pyodideInstance = await window.loadPyodide();
setPyodide(pyodideInstance);
setPyodideLoading(false);
return;
}
const existingScript = document.querySelector('script[src*="pyodide.js"]');
if (existingScript) {
existingScript.onload = async () => {
const pyodideInstance = await window.loadPyodide();
setPyodide(pyodideInstance);
setPyodideLoading(false);
};
return;
}
const script = document.createElement('script');
script.src = 'https://unpkg.com/pyodide@0.26.4/pyodide.js';
script.async = true;
script.onload = async () => {
try {
const pyodideInstance = await window.loadPyodide({
indexURL: 'https://unpkg.com/pyodide@0.26.4/'
});
setPyodide(pyodideInstance);
setPyodideLoading(false);
} catch (err) {
console.error('Pyodide init failed:', err);
setOutput('Error: Failed to initialize Python runtime\n');
setPyodideLoading(false);
}
};
script.onerror = () => {
console.error('Failed to load Pyodide script');
setOutput('Error: Failed to initialize Python runtime\n');
setPyodideLoading(false);
};
document.head.appendChild(script);
} catch (error) {
console.error('Failed to load Pyodide:', error);
setOutput('Error: Failed to initialize Python runtime\n');
setPyodideLoading(false);
}
};
initPyodide();
}, []);
/**
* Handles switching the active editor language.
* Updates the language state, swaps the editor content to the appropriate
* starter code, and re-registers the completion provider for the new language.
*
* @function
* @param {string} newLang - The language key to switch to (must be a key of {@link LANGUAGE_MAP}).
* @returns {void}
*/
const handleLanguageChange = (newLang) => {
setLanguage(newLang);
setCode(problem.starterCode[newLang] || '');
if (monacoRef.current) {
registerCompletionProvider(monacoRef.current, LANGUAGE_MAP[newLang]);
}
};
/**
* Executes the current editor code.
* - For Python: runs code in-browser via Pyodide, capturing stdout and stderr.
* The code is wrapped in a StringIO redirect so `print()` output is captured.
* - For other languages: simulates execution with a short timeout and a mock result.
*
* @async
* @function
* @returns {Promise<void>}
*/
const handleRunCode = async () => {
if (!pyodide) {
setOutput('Error: Python runtime not loaded yet. Please wait...\n');
return;
}
if (language !== 'python') {
setIsRunning(true);
setActiveTab('output');
setOutput('Running code...\n');
setTimeout(() => {
setOutput(`$ Running ${language} code...\n\nExecution complete.\n`);
setIsRunning(false);
}, 1500);
return;
}
setIsRunning(true);
setActiveTab('output');
setOutput('');
try {
const fullCode = `
import sys
from io import StringIO
# Redirect stdout and stderr
sys.stdout = StringIO()
sys.stderr = StringIO()
${code}
# Get the output
_stdout = sys.stdout.getvalue()
_stderr = sys.stderr.getvalue()
`;
await pyodide.runPythonAsync(fullCode);
const stdout = pyodide.globals.get('_stdout');
const stderr = pyodide.globals.get('_stderr');
let result = '';
if (stdout) result += stdout;
if (stderr) result += 'Error: ' + stderr;
setOutput(result || 'Code executed successfully (no output)\n');
} catch (error) {
setOutput(`Error executing Python code:\n${error.message}\n`);
} finally {
setIsRunning(false);
}
};
/**
* Handles solution submission.
* Displays a submission confirmation message in the output panel,
* then redirects to the dashboard via `onBack` after a short delay.
*
* @function
* @returns {void}
*/
const handleSubmit = () => {
setActiveTab('output');
setOutput(
'Submitting solution...\n\n' +
'Your solution has been submitted successfully.\n' +
'Redirecting to dashboard...'
);
setTimeout(() => onBack(), 2000);
};
return (
<div className="app">
<header className="app-header">
<div className="header-left">
<button className="btn-back" onClick={onBack}>
← Back
</button>
<h1 className="logo">AutoSuggestion Quiz</h1>
</div>
<div className="header-right">
<span className="problem-title">{problem.title}</span>
<button className="btn btn-outline" onClick={handleSubmit}>
Submit
</button>
</div>
</header>
<div className="main-layout">
<div className="panel problem-panel">
<div className="panel-header">
<span className="panel-title">Problem</span>
</div>
<div className="panel-body problem-body">
<h2 className="problem-heading">{problem.title}</h2>
<p className="problem-description">{problem.description}</p>
<div className="examples">
{problem.examples.map((ex, i) => (
<div key={i} className="example">
<h4>Example {i + 1}:</h4>
<pre className="example-block">
<strong>Input:</strong> {ex.input}
{'\n'}
<strong>Output:</strong> {ex.output}
{ex.explanation && (
<>
{'\n'}
<strong>Explanation:</strong> {ex.explanation}
</>
)}
</pre>
</div>
))}
</div>
</div>
</div>
<div className="panel editor-panel">
<div className="panel-header editor-header">
<div className="language-selector">
{Object.keys(LANGUAGE_MAP).map((lang) => (
<button
key={lang}
className={`lang-btn ${language === lang ? 'active' : ''}`}
onClick={() => handleLanguageChange(lang)}
>
{lang.charAt(0).toUpperCase() + lang.slice(1)}
</button>
))}
</div>
<div className="editor-actions">
<button
className="btn btn-run"
onClick={handleRunCode}
disabled={isRunning || (language === 'python' && pyodideLoading)}
>
{isRunning
? '⏳ Running...'
: language === 'python' && pyodideLoading
? '⏳ Loading Python...'
: '▶ Run Code'}
</button>
</div>
</div>
<div className="editor-container">
<Editor
height="100%"
language={LANGUAGE_MAP[language]}
value={code}
onChange={(value) => setCode(value || '')}
onMount={handleEditorDidMount}
theme="vs-dark"
options={{
fontSize: 14,
lineNumbers: 'on',
minimap: { enabled: false },
scrollBeyondLastLine: false,
automaticLayout: true,
tabSize: 4,
insertSpaces: true,
wordWrap: 'on',
padding: { top: 12 },
quickSuggestions: false,
suggestOnTriggerCharacters: false,
wordBasedSuggestions: 'off',
suggest: {
showIcons: true,
showStatusBar: true,
preview: true,
previewMode: 'subwordSmart',
shareSuggestSelections: false,
showInlineDetails: true,
filterGraceful: false,
},
folding: true,
bracketPairColorization: { enabled: true },
}}
/>
</div>
<div className="bottom-panel">
<div className="bottom-tabs">
<button
className={`tab-btn ${activeTab === 'output' ? 'active' : ''}`}
onClick={() => setActiveTab('output')}
>
Output
</button>
<button
className={`tab-btn ${activeTab === 'log' ? 'active' : ''}`}
onClick={() => setActiveTab('log')}
>
Suggestion Log
{suggestionLog.length > 0 && (
<span className="log-count">{suggestionLog.length}</span>
)}
</button>
</div>
<div className="bottom-content">
{activeTab === 'output' ? (
<pre className="output-text">
{output || 'Click "Run Code" to see output here.'}
</pre>
) : (
<div className="suggestion-log">
{suggestionLog.length === 0 ? (
<p className="log-empty">
No suggestions accepted yet. Start typing in the editor
and pause for 2 seconds — AI suggestions will appear as an
autocomplete dropdown. Select one to see it logged here.
</p>
) : (
suggestionLog.map((entry, i) => (
<div key={i} className="log-entry">
<span className="log-time">{entry.time}</span>
<span className="log-action">{entry.action}</span>
<span className="log-label">{entry.label}</span>
</div>
))
)}
</div>
)}
</div>
</div>
</div>
</div>
</div>
);
}
export default ProblemPage;