Merge pull request #6 from Danieli4/codex/add-python-programming-mission-module-ygunqb

Fix coding mission panel state handling
This commit is contained in:
Danil Gryaznev 2025-09-29 12:44:23 -06:00 committed by GitHub
commit 989a413162
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 43 additions and 32 deletions

View File

@ -1,6 +1,6 @@
'use client';
import { useEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { apiFetch } from '../lib/api';
interface CodingChallengeState {
@ -46,56 +46,59 @@ interface RunResult {
export function CodingMissionPanel({ missionId, token, initialState, initialCompleted = false }: CodingMissionPanelProps) {
const [state, setState] = useState<CodingMissionState | null>(initialState);
const [missionCompleted, setMissionCompleted] = useState(initialState?.is_mission_completed || initialCompleted);
const [activeChallengeId, setActiveChallengeId] = useState<number | null>(
initialState?.current_challenge_id ?? initialState?.challenges?.[0]?.id ?? null
);
const [editorCode, setEditorCode] = useState<string>(() => {
const active = initialState?.challenges.find((challenge) => challenge.id === activeChallengeId);
if (!active) {
return '';
}
return active.last_submitted_code ?? active.starter_code ?? '';
});
const [editorCode, setEditorCode] = useState<string>('');
const [status, setStatus] = useState<string | null>(null);
const [runResult, setRunResult] = useState<RunResult | null>(null);
const [loading, setLoading] = useState(false);
const previousChallengeIdRef = useRef<number | null>(null);
const activeChallenge = useMemo(() => {
if (!state || !state.challenges.length) {
return null;
}
const targetId = state.current_challenge_id ?? activeChallengeId;
if (targetId == null) {
return null;
if (state.current_challenge_id) {
const current = state.challenges.find((challenge) => challenge.id === state.current_challenge_id);
if (current) {
return current;
}
return state.challenges.find((challenge) => challenge.id === targetId) ?? null;
}, [state, activeChallengeId]);
}
const nextUnlocked = state.challenges.find((challenge) => challenge.is_unlocked && !challenge.is_passed);
if (nextUnlocked) {
return nextUnlocked;
}
const firstIncomplete = state.challenges.find((challenge) => !challenge.is_passed);
if (firstIncomplete) {
return firstIncomplete;
}
return state.challenges[state.challenges.length - 1];
}, [state]);
useEffect(() => {
if (!state || !state.challenges.length) {
if (!state) {
return;
}
const nextActiveId = state.current_challenge_id ?? state.challenges[state.challenges.length - 1].id;
if (nextActiveId !== activeChallengeId) {
setActiveChallengeId(nextActiveId);
const nextChallenge = state.challenges.find((challenge) => challenge.id === nextActiveId);
const nextCode = nextChallenge?.last_submitted_code ?? nextChallenge?.starter_code ?? '';
setEditorCode(nextCode);
setRunResult(null);
setStatus(null);
}
setMissionCompleted(state.is_mission_completed);
}, [state, activeChallengeId]);
}, [state]);
useEffect(() => {
if (!activeChallenge) {
previousChallengeIdRef.current = null;
return;
}
if (editorCode) {
if (previousChallengeIdRef.current === activeChallenge.id) {
return;
}
previousChallengeIdRef.current = activeChallenge.id;
const baseCode = activeChallenge.last_submitted_code ?? activeChallenge.starter_code ?? '';
setEditorCode(baseCode);
setRunResult(null);
setStatus(null);
}, [activeChallenge]);
if (!state) {
@ -113,10 +116,16 @@ export function CodingMissionPanel({ missionId, token, initialState, initialComp
if (!token) {
return;
}
try {
const updated = await apiFetch<CodingMissionState>(`/api/missions/${missionId}/coding/challenges`, {
authToken: token
});
setState(updated);
} catch (error) {
if (error instanceof Error) {
setStatus(error.message);
}
}
};
const handleRun = async () => {

View File

@ -78,6 +78,8 @@ export function OnboardingCarousel({ token, slides, initialCompletedOrder, nextO
border: '1px solid rgba(162, 155, 254, 0.25)'
}}
>
{/* В медиаконтенте могут встречаться внешние изображения, которые Next.js не умеет оптимизировать автоматически. */}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={currentSlide.media_url} alt={currentSlide.title} style={{ width: '100%', height: 'auto' }} />
</div>
)}