mirror of
https://github.com/R0m1k3/Reviflow.git
synced 2026-10-11 17:26:20 +02:00
Fix: Prevent recurring quiz title suffixes and improve dashboard widget loading
This commit is contained in:
1 parent
6531e3318d
commit
bab7d95968
3 files changed
+47
-15
No files matched your search
@@ -189,9 +189,9 @@ async def save_score(
|
||||
|
||||
# --- Remediation Logic ---
|
||||
# 1. If this IS a remediation quiz (detected by topic), close old pending errors
|
||||
# Robust check: case insensitive, check for 'remedia'
|
||||
# Robust check: case insensitive, check for 'remedia' or 'correct'
|
||||
topic_lower = score_data.topic.lower()
|
||||
if "remedia" in topic_lower:
|
||||
if "remedia" in topic_lower or "correct" in topic_lower:
|
||||
# Mark pending items for this revision/topic as REVIEWED
|
||||
# We assume that taking the quiz counts as reviewing them.
|
||||
# If they fail again, the code below (step 2) will add new items.
|
||||
@@ -531,9 +531,9 @@ async def generate_remediation_quiz(
|
||||
|
||||
# Force topic to indicate remediation/revision for reliable detection downstream
|
||||
topic_lower = quiz_content.get("topic", "").lower()
|
||||
if "remedia" not in topic_lower and "révision" not in topic_lower:
|
||||
if "remedia" not in topic_lower and "correct" not in topic_lower:
|
||||
original_topic = quiz_content.get("topic", "Révision")
|
||||
quiz_content["topic"] = f"{original_topic} (Révision)"
|
||||
quiz_content["topic"] = f"{original_topic} (Correction)"
|
||||
|
||||
# Inject revision_id if present so it persists through the quiz lifecycle
|
||||
if revision_id:
|
||||
@@ -572,7 +572,7 @@ async def get_mastery_stats(
|
||||
for s in scores:
|
||||
if not s.topic: continue
|
||||
# Normalize topic
|
||||
clean_topic = s.topic.replace(" (Remediation)", "").replace(" (Remédiation)", "").replace(" (Révision)", "").strip()
|
||||
clean_topic = s.topic.replace(" (Remediation)", "").replace(" (Correction)", "").replace(" (Remédiation)", "").replace(" (Révision)", "").strip()
|
||||
|
||||
if clean_topic not in topic_map:
|
||||
topic_map[clean_topic] = []
|
||||
@@ -595,7 +595,7 @@ async def get_mastery_stats(
|
||||
if not e.topic: continue
|
||||
# Also clean topic for errors if needed, or rely on exact match?
|
||||
# Ideally errors should also be grouped by clean topic.
|
||||
t_key = e.topic.replace(" (Remediation)", "").replace(" (Remédiation)", "").replace(" (Révision)", "").strip()
|
||||
t_key = e.topic.replace(" (Remediation)", "").replace(" (Correction)", "").replace(" (Remédiation)", "").replace(" (Révision)", "").strip()
|
||||
error_counts[t_key] = error_counts.get(t_key, 0) + 1
|
||||
|
||||
for topic, topic_scores in topic_map.items():
|
||||
|
||||
@@ -138,7 +138,11 @@ async def generate_remediation_quiz_service(context_items: List[Dict[str, Any]],
|
||||
|
||||
# Extract topics from context or questions to guide the LLM
|
||||
# This prevents it from thinking the topic is "Remediation"
|
||||
topics_context = list(set([item['context'] or "General" for item in context_items if item['context']]))
|
||||
def clean_topic_name(t):
|
||||
if not t: return "General"
|
||||
return t.replace(" (Remediation)", "").replace(" (Correction)", "").replace(" (Remédiation)", "").replace(" (Révision)", "").strip()
|
||||
|
||||
topics_context = list(set([clean_topic_name(item['context']) for item in context_items if item['context']]))
|
||||
joined_topics = ", ".join(topics_context)
|
||||
|
||||
errors_desc = "\n".join([
|
||||
@@ -163,7 +167,7 @@ async def generate_remediation_quiz_service(context_items: List[Dict[str, Any]],
|
||||
2. Identify the underlying concept for EACH failed question.
|
||||
3. Generate a NEW multiple-choice question to test that SAME concept, but phrased differently or using a different example.
|
||||
4. You MUST generate exactly one new question for every failed question provided in the input list. If there are 10 errors, generate 10 new questions.
|
||||
5. The "topic" field in the JSON should be: "{joined_topics} (Remediation)".
|
||||
5. The "topic" field in the JSON should be: "{joined_topics} (Correction)".
|
||||
6. **CRITICAL**: The questions must be about the SUBJECT MATTER ({joined_topics}). Do NOT generate questions about "remediation", "learning strategies", or "translation".
|
||||
7. **Language**: output MUST be in French.
|
||||
8. Provide a clear, helpful "explanation" for the correct answer.
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useNavigate } from 'react-router-dom';
|
||||
import { Play, BookOpen, ArrowRight, Book, AlertTriangle, X, Trash2, Sparkles, Lightbulb, Brain } from 'lucide-react';
|
||||
import { useAuth } from '../../stores/useAuth';
|
||||
import { api } from '../../lib/api';
|
||||
import { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Skeleton } from '../../components/ui/Skeleton';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
|
||||
@@ -51,18 +51,24 @@ export const RecentRevisionsWidget = () => {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!activeLearner) return;
|
||||
if (!activeLearner?.id) {
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
api.get<any>(`/quiz/stats/activity?learner_id=${activeLearner.id}`)
|
||||
setLoading(true);
|
||||
api.get<any>('/quiz/stats/activity', { params: { learner_id: activeLearner.id } })
|
||||
.then(res => {
|
||||
const allRevisions = res.data.history
|
||||
.flatMap((day: any) => day.items)
|
||||
.filter((item: any) => item.type === 'REVISION');
|
||||
setRevisions(allRevisions);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch(() => setLoading(false));
|
||||
}, [activeLearner]);
|
||||
.catch((err) => {
|
||||
console.error("Error fetching activity:", err);
|
||||
})
|
||||
.finally(() => setLoading(false));
|
||||
}, [activeLearner?.id]);
|
||||
|
||||
const getSubjectStyle = (subject?: string) => {
|
||||
if (subject === 'Mathématiques') return { color: 'text-blue-500', bg: 'bg-blue-50', icon: <BookOpen className="w-5 h-5" /> };
|
||||
@@ -129,8 +135,30 @@ export const RecentRevisionsWidget = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const renderEmptyState = () => (
|
||||
<div className="bg-white dark:bg-slate-800 rounded-[2rem] p-8 shadow-xl shadow-indigo-100/20 dark:shadow-none border border-slate-100 dark:border-slate-700 text-center">
|
||||
<div className="w-16 h-16 bg-slate-50 dark:bg-slate-700/50 rounded-2xl flex items-center justify-center mx-auto mb-4 text-3xl">
|
||||
📚
|
||||
</div>
|
||||
<h3 className="font-black text-lg text-slate-800 dark:text-white mb-2">
|
||||
Aucun cours récent
|
||||
</h3>
|
||||
<p className="text-slate-400 text-sm font-medium mb-6">
|
||||
Commencez une révision pour la voir apparaître ici !
|
||||
</p>
|
||||
<motion.button
|
||||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
onClick={() => navigate('/upload')}
|
||||
className="px-6 py-3 bg-indigo-600 text-white font-bold rounded-xl shadow-lg shadow-indigo-200 hover:bg-indigo-700 transition-all"
|
||||
>
|
||||
Ajouter un cours
|
||||
</motion.button>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (loading) return <Skeleton className="h-48 w-full rounded-[2rem]" />;
|
||||
if (revisions.length === 0) return null;
|
||||
if (revisions.length === 0) return renderEmptyState();
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
Reference in new issue
Block a user