Merge remote-tracking branch 'origin/master'

This commit is contained in:
Андрей Сумин
2025-03-03 14:04:09 +03:00
6 changed files with 107 additions and 47 deletions
+1 -3
View File
@@ -12,9 +12,7 @@ from apps.task.models import CompetitionTaskSubmission
def analyze_data_task(self, submission_id): def analyze_data_task(self, submission_id):
submission = CompetitionTaskSubmission.objects.get(id=submission_id) submission = CompetitionTaskSubmission.objects.get(id=submission_id)
try: try:
code_url = ( code_url = f"{settings.MINIO_DEFAULT_CUSTOM_ENDPOINT_URL}{submission.content.path}"
f"{settings.MINIO_DEFAULT_CUSTOM_ENDPOINT_URL}{submission.path}"
)
files = [ files = [
{ {
"url": f"{settings.MINIO_DEFAULT_CUSTOM_ENDPOINT_URL}{attachment.path}", "url": f"{settings.MINIO_DEFAULT_CUSTOM_ENDPOINT_URL}{attachment.path}",
@@ -7,13 +7,23 @@ interface CompetitionHeaderProps {
title: string; title: string;
tasks: Task[]; tasks: Task[];
competitionId: string; competitionId: string;
setAnswer: (value: string) => void;
setSelectedFile: (file: File | null) => void; // заглушка
} }
const CompetitionHeader: React.FC<CompetitionHeaderProps> = ({ const CompetitionHeader: React.FC<CompetitionHeaderProps> = ({
title, title,
tasks, tasks,
competitionId competitionId,
setAnswer,
setSelectedFile
}) => { }) => {
const handleTaskSelect = () => {
setAnswer("")
setSelectedFile(null)
}
return ( return (
<header className="bg-white shadow-sm sticky top-0 z-30 w-full"> <header className="bg-white shadow-sm sticky top-0 z-30 w-full">
<div className="mx-auto max-w-6xl px-4"> <div className="mx-auto max-w-6xl px-4">
@@ -21,6 +31,7 @@ const CompetitionHeader: React.FC<CompetitionHeaderProps> = ({
<Link <Link
to={`/competition/${competitionId}`} to={`/competition/${competitionId}`}
className="flex items-center text-gray-600 hover:text-gray-900 transition-colors font-hse-sans text-sm" className="flex items-center text-gray-600 hover:text-gray-900 transition-colors font-hse-sans text-sm"
onClick={handleTaskSelect}
> >
<ArrowLeft className="h-4 w-4 mr-1" /> <ArrowLeft className="h-4 w-4 mr-1" />
</Link> </Link>
@@ -95,6 +95,8 @@ const CompetitionSession = () => {
title={competitionTitle} title={competitionTitle}
tasks={tasks} tasks={tasks}
competitionId={competitionId} competitionId={competitionId}
setAnswer={setAnswer}
setSelectedFile={setSelectedFile}
/> />
<main className="flex-1 bg-[#F8F8F8] pb-8"> <main className="flex-1 bg-[#F8F8F8] pb-8">
@@ -11,7 +11,7 @@ interface SolutionHistorySheetProps {
solutions: Solution[]; solutions: Solution[];
maxPoints: number; maxPoints: number;
onSolutionSelect: (solution: Solution) => void; onSolutionSelect: (solution: Solution) => void;
currentSolutionId?: string | null; currentSolutionId?: string;
} }
const SolutionHistorySheet: React.FC<SolutionHistorySheetProps> = ({ const SolutionHistorySheet: React.FC<SolutionHistorySheetProps> = ({
@@ -22,7 +22,6 @@ const SolutionHistorySheet: React.FC<SolutionHistorySheetProps> = ({
onSolutionSelect, onSolutionSelect,
currentSolutionId currentSolutionId
}) => { }) => {
return ( return (
<Sheet open={isOpen} onOpenChange={onOpenChange}> <Sheet open={isOpen} onOpenChange={onOpenChange}>
<SheetContent className="w-[350px] sm:w-[450px] p-0"> <SheetContent className="w-[350px] sm:w-[450px] p-0">
@@ -39,11 +38,15 @@ const SolutionHistorySheet: React.FC<SolutionHistorySheetProps> = ({
<div className="flex flex-col mt-3 space-y-2.5 overflow-y-auto max-h-[calc(100vh-80px)] px-4 pb-4"> <div className="flex flex-col mt-3 space-y-2.5 overflow-y-auto max-h-[calc(100vh-80px)] px-4 pb-4">
{solutions.length > 0 ? ( {solutions.length > 0 ? (
solutions.map((solution) => ( solutions.map((solution, index) => (
<div <div
key={solution.id} key={solution.id || index}
className={`w-full cursor-pointer relative ${solution.id === currentSolutionId ? 'ring-2 ring-blue-500 rounded-lg' : ''}`} className={`w-full cursor-pointer transition-transform hover:scale-[1.01] relative
onClick={() => onSolutionSelect(solution)} ${solution.id === currentSolutionId ? 'ring-2 ring-blue-500 rounded-lg' : ''}`}
onClick={() => {
onSolutionSelect(solution);
onOpenChange(false);
}}
> >
{solution.id === currentSolutionId && ( {solution.id === currentSolutionId && (
<div className="absolute top-2 right-2 z-10 bg-blue-500 text-white rounded-full p-1"> <div className="absolute top-2 right-2 z-10 bg-blue-500 text-white rounded-full p-1">
@@ -11,6 +11,7 @@
const SolutionStatus: React.FC<SolutionStatusProps> = ({ solution, maxPoints }) => { const SolutionStatus: React.FC<SolutionStatusProps> = ({ solution, maxPoints }) => {
const formattedDate = solution.timestamp ? format(parseISO(solution.timestamp), "d MMMM, HH:mm", { locale: ru }) : ''; const formattedDate = solution.timestamp ? format(parseISO(solution.timestamp), "d MMMM, HH:mm", { locale: ru }) : '';
console.log(solution, "SOLUTION STATUS")
return ( return (
<div className={`${getSolutionBgColor(solution.status, solution.earned_points, maxPoints)} rounded-lg p-4 relative`}> <div className={`${getSolutionBgColor(solution.status, solution.earned_points, maxPoints)} rounded-lg p-4 relative`}>
<div className="flex flex-col"> <div className="flex flex-col">
@@ -9,6 +9,8 @@ import FileSolution from './components/FileSolution';
import CodeSolution from './components/CodeSolution'; import CodeSolution from './components/CodeSolution';
import ActionButtons from './components/ActionButtons'; import ActionButtons from './components/ActionButtons';
import SolutionHistorySheet from './components/SolutionHistorySheet'; import SolutionHistorySheet from './components/SolutionHistorySheet';
import { AlertTriangle, ArrowRight } from 'lucide-react';
import { Button } from '@/components/ui/button';
interface TaskSolutionProps { interface TaskSolutionProps {
task: Task; task: Task;
@@ -17,6 +19,7 @@ interface TaskSolutionProps {
selectedFile: File | null; selectedFile: File | null;
setSelectedFile: (file: File | null) => void; setSelectedFile: (file: File | null) => void;
onSubmit: () => void; onSubmit: () => void;
isSubmitting?: boolean;
} }
const TaskSolution: React.FC<TaskSolutionProps> = ({ const TaskSolution: React.FC<TaskSolutionProps> = ({
@@ -26,13 +29,14 @@ const TaskSolution: React.FC<TaskSolutionProps> = ({
selectedFile, selectedFile,
setSelectedFile, setSelectedFile,
onSubmit, onSubmit,
isSubmitting = false
}) => { }) => {
const fileInputRef = useRef<HTMLInputElement>(null); const fileInputRef = useRef<HTMLInputElement>(null);
const [isHistoryOpen, setIsHistoryOpen] = useState(false); const [isHistoryOpen, setIsHistoryOpen] = useState(false);
const [selectedSolutionUrl, setSelectedSolutionUrl] = useState<string | null>(null); const [selectedSolutionUrl, setSelectedSolutionUrl] = useState<string | null>(null);
const [currentSolution, setCurrentSolution] = useState<Solution | null>(null); const [displayedSolution, setDisplayedSolution] = useState<Solution | null>(null);
const { id: competitionId } = useParams<{ id: string }>(); const { id: competitionId } = useParams<{ id: string }>();
const taskIdRef = useRef<string | null>(null); const prevTaskIdRef = useRef<string | null>(null);
const solutionsQuery = useQuery({ const solutionsQuery = useQuery({
queryKey: ['solutionHistory', competitionId, task.id], queryKey: ['solutionHistory', competitionId, task.id],
@@ -41,48 +45,66 @@ const TaskSolution: React.FC<TaskSolutionProps> = ({
}); });
const solutionHistory = solutionsQuery.data || []; const solutionHistory = solutionsQuery.data || [];
// Handle task changes
const getLatestSolution = () => {
return solutionHistory.length > 0 ? solutionHistory[solutionHistory.length - 1] : null;
};
const isOutdatedSolution = () => {
if (!displayedSolution || solutionHistory.length === 0) return false;
const latestSolution = getLatestSolution();
return latestSolution?.id !== displayedSolution.id;
};
// Set initial solution to the last one (most recent) when solutions are loaded
useEffect(() => { useEffect(() => {
if (taskIdRef.current !== task.id) { if (solutionHistory.length > 0 && !displayedSolution) {
setCurrentSolution(null); const latestSolution = solutionHistory[solutionHistory.length - 1];
setDisplayedSolution(latestSolution);
}
}, [solutionHistory, displayedSolution]);
// When task changes, reset everything and load the latest solution for the new task
useEffect(() => {
if (prevTaskIdRef.current !== task.id) {
// Reset states for new task
setDisplayedSolution(null);
setSelectedSolutionUrl(null); setSelectedSolutionUrl(null);
setAnswer("");
setSelectedFile(null);
taskIdRef.current = task.id;
// Wait for the query to complete // If solutions are already loaded for the new task, set the latest one
if (!solutionsQuery.isLoading && solutionHistory.length > 0) { if (solutionHistory.length > 0) {
// Get the most recent solution (last in the array)
const latestSolution = solutionHistory[solutionHistory.length - 1]; const latestSolution = solutionHistory[solutionHistory.length - 1];
setCurrentSolution(latestSolution); setDisplayedSolution(latestSolution);
} }
prevTaskIdRef.current = task.id;
} }
}, [task.id, solutionHistory, solutionsQuery.isLoading, setAnswer, setSelectedFile]); }, [task.id, solutionHistory]);
// Refresh current solution when the solution history changes (after a new submission) // Check if a new solution was submitted (latest solution ID changed)
useEffect(() => { useEffect(() => {
if (!solutionsQuery.isLoading && solutionHistory.length > 0) { if (solutionHistory.length > 0 && displayedSolution) {
// If we don't have a current solution or there's a new submission const latestSolution = solutionHistory[solutionHistory.length - 1];
// (which would be the last item in the array)
if (!currentSolution || // If the latest solution ID is different from the displayed one,
currentSolution.id !== solutionHistory[solutionHistory.length - 1].id) { // a new solution was submitted - update to show the latest
// Set to the latest solution (last in the array) if (latestSolution.id !== displayedSolution.id) {
setCurrentSolution(solutionHistory[solutionHistory.length - 1]); setDisplayedSolution(latestSolution);
} }
} }
}, [solutionHistory, currentSolution, solutionsQuery.isLoading]); }, [solutionHistory, displayedSolution]);
// Load solution content when current solution changes // Load solution content when the displayed solution changes
useEffect(() => { useEffect(() => {
const loadSolutionContent = async () => { const loadSolutionContent = async () => {
if (!currentSolution || !currentSolution.content) return; if (!displayedSolution || !displayedSolution.content) return;
try { try {
if (task.type === TaskType.FILE) { if (task.type === TaskType.FILE) {
setSelectedFile(null); setSelectedFile(null);
setSelectedSolutionUrl(currentSolution.content); setSelectedSolutionUrl(displayedSolution.content);
} else { } else {
const response = await fetch(currentSolution.content); const response = await fetch(displayedSolution.content);
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to fetch solution content: ${response.status}`); throw new Error(`Failed to fetch solution content: ${response.status}`);
} }
@@ -95,39 +117,61 @@ const TaskSolution: React.FC<TaskSolutionProps> = ({
}; };
loadSolutionContent(); loadSolutionContent();
}, [currentSolution, task.type, setAnswer, setSelectedFile]); }, [displayedSolution, task.type, setAnswer, setSelectedFile]);
const handleOpenHistory = () => { const handleOpenHistory = () => {
setIsHistoryOpen(true); setIsHistoryOpen(true);
}; };
const handleSolutionSelect = (solution: Solution) => { const handleSolutionSelect = (solution: Solution) => {
setCurrentSolution(solution); setDisplayedSolution(solution);
setIsHistoryOpen(false);
}; };
const handleClearExistingFile = () => { const handleClearExistingFile = () => {
setSelectedSolutionUrl(null); setSelectedSolutionUrl(null);
}; };
const handleSubmitWrapper = () => { // Function to switch to the latest solution
onSubmit(); const goToLatestSolution = () => {
const latestSolution = getLatestSolution();
if (latestSolution) {
setDisplayedSolution(latestSolution);
}
}; };
return ( return (
<div className="md:w-[500px] flex flex-col gap-4"> <div className="md:w-[500px] flex flex-col gap-4">
{currentSolution ? ( {displayedSolution ? (
<SolutionStatus solution={currentSolution} maxPoints={task.points}/> <SolutionStatus solution={displayedSolution} maxPoints={task.points}/>
) : ( ) : (
<div className="bg-gray-100 rounded-lg p-4 text-gray-600 font-hse-sans"> <div className="bg-gray-100 rounded-lg p-4 text-gray-600 font-hse-sans">
Решение еще не отправлено Решение еще не отправлено
</div> </div>
)} )}
{/* Outdated solution warning */}
{isOutdatedSolution() && (
<div className="bg-amber-50 border border-amber-200 rounded-lg p-3 flex justify-between items-center">
<div className="flex items-center text-amber-800">
<AlertTriangle size={18} className="mr-2 text-amber-500" />
<span className="font-hse-sans text-sm">Устаревшая посылка</span>
</div>
<Button
variant="ghost"
size="sm"
className="text-blue-600 hover:text-blue-700 hover:bg-blue-50 flex items-center"
onClick={goToLatestSolution}
>
<span className="mr-1">К последней</span>
<ArrowRight size={16} />
</Button>
</div>
)}
{task.type === TaskType.INPUT && ( {task.type === TaskType.INPUT && (
<InputSolution <InputSolution
answer={answer} answer={answer}
setAnswer={setAnswer} setAnswer={setAnswer}
/> />
)} )}
@@ -138,6 +182,7 @@ const TaskSolution: React.FC<TaskSolutionProps> = ({
fileInputRef={fileInputRef} fileInputRef={fileInputRef}
existingFileUrl={selectedSolutionUrl} existingFileUrl={selectedSolutionUrl}
onClearExistingFile={handleClearExistingFile} onClearExistingFile={handleClearExistingFile}
isLoading={isSubmitting}
/> />
)} )}
@@ -149,7 +194,7 @@ const TaskSolution: React.FC<TaskSolutionProps> = ({
)} )}
<ActionButtons <ActionButtons
onSubmit={handleSubmitWrapper} onSubmit={onSubmit}
onHistoryClick={handleOpenHistory} onHistoryClick={handleOpenHistory}
/> />
@@ -159,7 +204,7 @@ const TaskSolution: React.FC<TaskSolutionProps> = ({
solutions={solutionHistory} solutions={solutionHistory}
maxPoints={task.points} maxPoints={task.points}
onSolutionSelect={handleSolutionSelect} onSolutionSelect={handleSolutionSelect}
currentSolutionId={currentSolution?.id} currentSolutionId={displayedSolution?.id}
/> />
</div> </div>
); );