mirror of
https://gitlab.com/megazordpobeda/DataRush.git
synced 2026-05-23 05:07:10 +00:00
Merge remote-tracking branch 'origin/master'
This commit is contained in:
@@ -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}",
|
||||||
|
|||||||
+12
-1
@@ -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">
|
||||||
|
|||||||
+9
-6
@@ -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">
|
||||||
|
|||||||
+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);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}, [task.id, solutionHistory, solutionsQuery.isLoading, setAnswer, setSelectedFile]);
|
|
||||||
|
|
||||||
// Refresh current solution when the solution history changes (after a new submission)
|
prevTaskIdRef.current = task.id;
|
||||||
|
}
|
||||||
|
}, [task.id, solutionHistory]);
|
||||||
|
|
||||||
|
// 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,35 +117,57 @@ 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}
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user