Mehdi commited on
Commit
7941f4d
·
1 Parent(s): bb37799

feat: thème éducatif natif Gradio + traduction UI FR/EN dynamique

Browse files
Files changed (1) hide show
  1. app.py +144 -100
app.py CHANGED
@@ -11,6 +11,47 @@ from core.chunker import chunk_text
11
  from core.questioner import generate_question
12
  from core.evaluator import evaluate_answer
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  # ---------------------------------------------------------------------------
15
  # State helpers
16
  # ---------------------------------------------------------------------------
@@ -27,9 +68,24 @@ def _score_label(correct: int, total: int) -> str:
27
  return f"✅ {correct} / {total}" if total > 0 else "—"
28
 
29
 
30
- def load_pdf(pdf_file):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  if pdf_file is None:
32
- return [], "Aucun fichier sélectionné.", 0, 0
33
  try:
34
  text = extract_text(pdf_file.name)
35
  chunks = chunk_text(text)
@@ -38,14 +94,15 @@ def load_pdf(pdf_file):
38
  except Exception as e:
39
  return [], f"Erreur inattendue : {e}", 0, 0
40
  if not chunks:
41
- return [], "PDF chargé mais aucun contenu textuel trouvé (document trop court ou scanné ?).", 0, 0
42
- return chunks, f"📚 {len(chunks)} sections extraites — prêt à générer des questions !", 0, 0
43
 
44
 
45
  @spaces.GPU(duration=60)
46
  def new_question(chunks, language):
 
47
  if not chunks:
48
- return "", "", "⚠️ Charge d'abord un PDF."
49
  try:
50
  chunk = random.choice(chunks)
51
  question = generate_question(chunk, language=language)
@@ -56,10 +113,11 @@ def new_question(chunks, language):
56
 
57
  @spaces.GPU(duration=120)
58
  def submit_answer(question, chunk, student_answer, correct, total, language):
 
59
  if not question:
60
- return "⚠️ Génère d'abord une question.", correct, total, _score_label(correct, total)
61
  if not student_answer.strip():
62
- return "⚠️ Écris une réponse avant de valider.", correct, total, _score_label(correct, total)
63
  try:
64
  feedback = evaluate_answer(question, chunk, student_answer, language=language)
65
  total += 1
@@ -71,140 +129,126 @@ def submit_answer(question, chunk, student_answer, correct, total, language):
71
 
72
 
73
  # ---------------------------------------------------------------------------
74
- # Custom CSS — style Lumni (fond blanc, orange + bleu marine, Nunito)
75
  # ---------------------------------------------------------------------------
76
 
77
- CSS = """
78
- @import url('https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800;900&display=swap');
79
-
80
- * { font-family: 'Nunito', sans-serif !important; }
81
- :root {
82
- color-scheme: light !important;
83
- /* Override variables Gradio Soft */
84
- --block-background-fill: #FFFFFF !important;
85
- --panel-background-fill: #FFFFFF !important;
86
- --background-fill-primary: #FFF5D6 !important;
87
- --background-fill-secondary: #FFFFFF !important;
88
- --input-background-fill: #FFFEF8 !important;
89
- --block-border-color: #E8DFB8 !important;
90
- --border-color-primary: #E8DFB8 !important;
91
- --color-accent: #F96A0A !important;
92
- --button-primary-background-fill: #F96A0A !important;
93
- --button-secondary-background-fill: #3D3BD6 !important;
94
- }
95
-
96
- /* Fond beige Lumni */
97
- body, .gradio-container { background: #FFF5D6 !important; }
98
-
99
- /* Neutralise le rendu "chip" des labels du thème Soft */
100
- label > span {
101
- background: transparent !important;
102
- color: #3D3BD6 !important;
103
- font-weight: 700 !important;
104
- font-size: 0.9rem !important;
105
- padding: 0 !important;
106
- border-radius: 0 !important;
107
- box-shadow: none !important;
108
- text-transform: none !important;
109
- letter-spacing: normal !important;
110
- }
 
 
 
 
 
 
 
111
 
112
- /* Boutons pilule */
113
- button.lg {
114
- border-radius: 50px !important;
115
- font-weight: 800 !important;
116
- font-size: 1rem !important;
117
- transition: all 0.15s ease !important;
118
- }
119
- button.primary { background: #F96A0A !important; color: #fff !important; border: none !important; }
120
- button.primary:hover { background: #D95800 !important; transform: translateY(-1px) !important; box-shadow: 0 6px 14px rgba(249,106,10,0.3) !important; }
121
- button.secondary { background: #3D3BD6 !important; color: #fff !important; border: none !important; }
122
- button.secondary:hover { background: #2B29A8 !important; transform: translateY(-1px) !important; box-shadow: 0 6px 14px rgba(61,59,214,0.25) !important; }
123
-
124
- /* Inputs */
125
- textarea, input[type=text] {
126
- border-radius: 12px !important;
127
- border: 2px solid #E8DFB8 !important;
128
- background: #FFFEF8 !important;
129
- color: #1A1A2E !important;
130
- font-size: 0.97rem !important;
131
- }
132
- textarea:focus { border-color: #F96A0A !important; box-shadow: 0 0 0 3px rgba(249,106,10,0.1) !important; }
133
 
134
- /* Score */
135
  #score-box textarea {
136
- background: #3D3BD6 !important; color: #fff !important;
137
- font-weight: 900 !important; font-size: 1.4rem !important;
138
- text-align: center !important; border: none !important;
 
 
 
139
  }
140
-
141
- /* Status */
142
- #status-box textarea { background: #EEEEFF !important; border-color: #3D3BD6 !important; color: #1A1A8E !important; font-weight: 600 !important; }
143
-
144
- /* Question */
145
- #question-box textarea { background: #FFF3E8 !important; border-color: #F96A0A !important; font-weight: 700 !important; }
146
-
147
- /* Feedback */
148
  #feedback-box textarea { background: #F0FFF6 !important; border-color: #00B08D !important; line-height: 1.75 !important; }
149
-
150
- footer { display: none !important; }
151
  """
152
 
153
- HEADER_HTML = """
154
- <div id="paperprof-header">
155
- <span style="font-size:3rem;line-height:1">📄</span>
156
  <div>
157
- <div style="color:white;font-size:2rem;font-weight:900;font-family:Nunito,sans-serif;letter-spacing:-0.02em;line-height:1.1">
158
- PaperProf
159
- </div>
160
- <div style="color:rgba(255,255,255,0.82);font-size:1rem;font-family:Nunito,sans-serif;margin-top:5px">
161
- Révise tes cours avec l'IA &mdash; questions, réponses, feedback instantané
162
- </div>
163
  </div>
164
  </div>
165
  """
166
 
167
  # ---------------------------------------------------------------------------
168
- # Gradio UI
169
  # ---------------------------------------------------------------------------
170
 
171
- with gr.Blocks(title="PaperProf", css=CSS, theme=gr.themes.Soft()) as demo:
172
 
173
- gr.HTML(HEADER_HTML)
174
 
175
- chunks_state = gr.State([])
176
- chunk_state = gr.State("")
177
  correct_state = gr.State(0)
178
- total_state = gr.State(0)
179
 
180
  with gr.Row():
181
- pdf_input = gr.File(label="📎 Cours PDF", file_types=[".pdf"], scale=3)
182
- load_btn = gr.Button("Charger le PDF", variant="primary", scale=2)
183
  language_selector = gr.Radio(["English", "Français"], value="Français", label="🌐 Langue")
184
 
185
  with gr.Row():
186
  load_status = gr.Textbox(label="Statut", interactive=False, scale=4, elem_id="status-box")
187
- score_box = gr.Textbox(label="Score", value="—", interactive=False, scale=1, elem_id="score-box")
188
 
189
  with gr.Row():
190
  question_box = gr.Textbox(label="❓ Question", interactive=False, lines=3, scale=3, elem_id="question-box")
191
- new_q_btn = gr.Button("Nouvelle question", variant="secondary", scale=1)
192
 
193
- answer_box = gr.Textbox(label="✏️ Ta réponse", lines=4, placeholder="Écris ta réponse ici…")
194
- submit_btn = gr.Button("Valider la réponse", variant="primary")
195
  feedback_box = gr.Textbox(label="💬 Feedback", interactive=False, lines=7, elem_id="feedback-box")
196
 
197
- # Wire events
 
 
 
 
 
 
198
  load_btn.click(
199
  load_pdf,
200
- inputs=[pdf_input],
201
  outputs=[chunks_state, load_status, correct_state, total_state],
202
  ).then(lambda: "—", outputs=[score_box])
 
203
  new_q_btn.click(
204
  new_question,
205
  inputs=[chunks_state, language_selector],
206
  outputs=[question_box, chunk_state, feedback_box],
207
  )
 
208
  submit_btn.click(
209
  submit_answer,
210
  inputs=[question_box, chunk_state, answer_box, correct_state, total_state, language_selector],
 
11
  from core.questioner import generate_question
12
  from core.evaluator import evaluate_answer
13
 
14
+ # ---------------------------------------------------------------------------
15
+ # Internationalisation
16
+ # ---------------------------------------------------------------------------
17
+
18
+ UI = {
19
+ "English": {
20
+ "load_btn": "Load PDF",
21
+ "new_q_btn": "New Question",
22
+ "submit_btn": "Submit Answer",
23
+ "pdf_label": "📎 Course PDF",
24
+ "status_label": "Status",
25
+ "question_label": "❓ Question",
26
+ "answer_label": "✏️ Your Answer",
27
+ "answer_ph": "Write your answer here…",
28
+ "feedback_label": "💬 Feedback",
29
+ "no_file": "No file selected.",
30
+ "no_chunks": "PDF loaded but no text found (scanned or too short?).",
31
+ "loaded": lambda n: f"📚 {n} sections extracted — ready to generate questions!",
32
+ "no_pdf": "⚠️ Please upload a PDF first.",
33
+ "no_question": "⚠️ Generate a question first.",
34
+ "no_answer": "⚠️ Please write an answer before submitting.",
35
+ },
36
+ "Français": {
37
+ "load_btn": "Charger le PDF",
38
+ "new_q_btn": "Nouvelle question",
39
+ "submit_btn": "Valider la réponse",
40
+ "pdf_label": "📎 Cours PDF",
41
+ "status_label": "Statut",
42
+ "question_label": "❓ Question",
43
+ "answer_label": "✏️ Ta réponse",
44
+ "answer_ph": "Écris ta réponse ici…",
45
+ "feedback_label": "💬 Feedback",
46
+ "no_file": "Aucun fichier sélectionné.",
47
+ "no_chunks": "PDF chargé mais aucun texte trouvé (scanné ou trop court ?).",
48
+ "loaded": lambda n: f"📚 {n} sections extraites — prêt à générer des questions !",
49
+ "no_pdf": "⚠️ Charge d'abord un PDF.",
50
+ "no_question": "⚠️ Génère d'abord une question.",
51
+ "no_answer": "⚠️ Écris une réponse avant de valider.",
52
+ },
53
+ }
54
+
55
  # ---------------------------------------------------------------------------
56
  # State helpers
57
  # ---------------------------------------------------------------------------
 
68
  return f"✅ {correct} / {total}" if total > 0 else "—"
69
 
70
 
71
+ def update_ui_language(language):
72
+ s = UI[language]
73
+ return (
74
+ gr.update(value=s["load_btn"]),
75
+ gr.update(value=s["new_q_btn"]),
76
+ gr.update(value=s["submit_btn"]),
77
+ gr.update(label=s["pdf_label"]),
78
+ gr.update(label=s["status_label"]),
79
+ gr.update(label=s["question_label"]),
80
+ gr.update(label=s["answer_label"], placeholder=s["answer_ph"]),
81
+ gr.update(label=s["feedback_label"]),
82
+ )
83
+
84
+
85
+ def load_pdf(pdf_file, language):
86
+ s = UI[language]
87
  if pdf_file is None:
88
+ return [], s["no_file"], 0, 0
89
  try:
90
  text = extract_text(pdf_file.name)
91
  chunks = chunk_text(text)
 
94
  except Exception as e:
95
  return [], f"Erreur inattendue : {e}", 0, 0
96
  if not chunks:
97
+ return [], s["no_chunks"], 0, 0
98
+ return chunks, s["loaded"](len(chunks)), 0, 0
99
 
100
 
101
  @spaces.GPU(duration=60)
102
  def new_question(chunks, language):
103
+ s = UI[language]
104
  if not chunks:
105
+ return "", "", s["no_pdf"]
106
  try:
107
  chunk = random.choice(chunks)
108
  question = generate_question(chunk, language=language)
 
113
 
114
  @spaces.GPU(duration=120)
115
  def submit_answer(question, chunk, student_answer, correct, total, language):
116
+ s = UI[language]
117
  if not question:
118
+ return s["no_question"], correct, total, _score_label(correct, total)
119
  if not student_answer.strip():
120
+ return s["no_answer"], correct, total, _score_label(correct, total)
121
  try:
122
  feedback = evaluate_answer(question, chunk, student_answer, language=language)
123
  total += 1
 
129
 
130
 
131
  # ---------------------------------------------------------------------------
132
+ # Thème éducatif
133
  # ---------------------------------------------------------------------------
134
 
135
+ edu_theme = gr.themes.Base(
136
+ font=gr.themes.GoogleFont("Nunito"),
137
+ font_mono=gr.themes.GoogleFont("Nunito"),
138
+ ).set(
139
+ # Fond général
140
+ body_background_fill="#FFF8DC",
141
+ body_text_color="#1A1A2E",
142
+ # Cards
143
+ block_background_fill="#FFFFFF",
144
+ block_border_color="#F0E0A0",
145
+ block_border_width="2px",
146
+ block_radius="18px",
147
+ block_shadow="0 2px 16px rgba(0,0,0,0.07)",
148
+ block_label_text_color="#3D3BD6",
149
+ block_label_text_size="sm",
150
+ block_label_text_weight="700",
151
+ # Inputs
152
+ input_background_fill="#FFFEF5",
153
+ input_border_color="#F0E0A0",
154
+ input_radius="12px",
155
+ # Boutons primary (orange)
156
+ button_primary_background_fill="#F96A0A",
157
+ button_primary_background_fill_hover="#D95800",
158
+ button_primary_text_color="#FFFFFF",
159
+ button_primary_border_color="#F96A0A",
160
+ button_large_radius="50px",
161
+ button_large_padding="12px 28px",
162
+ button_large_text_weight="800",
163
+ button_large_text_size="md",
164
+ # Boutons secondary (bleu)
165
+ button_secondary_background_fill="#3D3BD6",
166
+ button_secondary_background_fill_hover="#2B29A8",
167
+ button_secondary_text_color="#FFFFFF",
168
+ button_secondary_border_color="#3D3BD6",
169
+ # Panel
170
+ panel_background_fill="#FFFFFF",
171
+ panel_border_color="#F0E0A0",
172
+ # Slider / Radio
173
+ color_accent="#F96A0A",
174
+ color_accent_soft="#FFF3E8",
175
+ )
176
 
177
+ CSS = """
178
+ footer { display: none !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
 
 
180
  #score-box textarea {
181
+ background: #3D3BD6 !important;
182
+ color: #fff !important;
183
+ font-weight: 900 !important;
184
+ font-size: 1.4rem !important;
185
+ text-align: center !important;
186
+ border: none !important;
187
  }
188
+ #status-box textarea { background: #EEF0FF !important; color: #2B29A8 !important; font-weight: 600 !important; }
189
+ #question-box textarea { background: #FFF3E8 !important; border-color: #F96A0A !important; font-weight: 700 !important; font-size: 1.05rem !important; }
 
 
 
 
 
 
190
  #feedback-box textarea { background: #F0FFF6 !important; border-color: #00B08D !important; line-height: 1.75 !important; }
 
 
191
  """
192
 
193
+ HEADER = """
194
+ <div style="background:linear-gradient(135deg,#3D3BD6,#6B69FF);border-radius:20px;padding:24px 28px;margin-bottom:4px;display:flex;align-items:center;gap:18px">
195
+ <span style="font-size:2.8rem">📄</span>
196
  <div>
197
+ <div style="color:#fff;font-size:1.9rem;font-weight:900;font-family:Nunito,sans-serif;line-height:1.1">PaperProf</div>
198
+ <div style="color:rgba(255,255,255,.82);font-size:0.95rem;font-family:Nunito,sans-serif;margin-top:4px">Révise tes cours avec l'IA · questions · réponses · feedback instantané</div>
 
 
 
 
199
  </div>
200
  </div>
201
  """
202
 
203
  # ---------------------------------------------------------------------------
204
+ # UI
205
  # ---------------------------------------------------------------------------
206
 
207
+ with gr.Blocks(title="PaperProf", theme=edu_theme, css=CSS) as demo:
208
 
209
+ gr.HTML(HEADER)
210
 
211
+ chunks_state = gr.State([])
212
+ chunk_state = gr.State("")
213
  correct_state = gr.State(0)
214
+ total_state = gr.State(0)
215
 
216
  with gr.Row():
217
+ pdf_input = gr.File(label="📎 Cours PDF", file_types=[".pdf"], scale=3)
218
+ load_btn = gr.Button("Charger le PDF", variant="primary", scale=2)
219
  language_selector = gr.Radio(["English", "Français"], value="Français", label="🌐 Langue")
220
 
221
  with gr.Row():
222
  load_status = gr.Textbox(label="Statut", interactive=False, scale=4, elem_id="status-box")
223
+ score_box = gr.Textbox(label="Score", value="—", interactive=False, scale=1, elem_id="score-box")
224
 
225
  with gr.Row():
226
  question_box = gr.Textbox(label="❓ Question", interactive=False, lines=3, scale=3, elem_id="question-box")
227
+ new_q_btn = gr.Button("Nouvelle question", variant="secondary", scale=1)
228
 
229
+ answer_box = gr.Textbox(label="✏️ Ta réponse", lines=4, placeholder="Écris ta réponse ici…")
230
+ submit_btn = gr.Button("Valider la réponse", variant="primary")
231
  feedback_box = gr.Textbox(label="💬 Feedback", interactive=False, lines=7, elem_id="feedback-box")
232
 
233
+ # Traduction UI quand on change la langue
234
+ language_selector.change(
235
+ update_ui_language,
236
+ inputs=[language_selector],
237
+ outputs=[load_btn, new_q_btn, submit_btn, pdf_input, load_status, question_box, answer_box, feedback_box],
238
+ )
239
+
240
  load_btn.click(
241
  load_pdf,
242
+ inputs=[pdf_input, language_selector],
243
  outputs=[chunks_state, load_status, correct_state, total_state],
244
  ).then(lambda: "—", outputs=[score_box])
245
+
246
  new_q_btn.click(
247
  new_question,
248
  inputs=[chunks_state, language_selector],
249
  outputs=[question_box, chunk_state, feedback_box],
250
  )
251
+
252
  submit_btn.click(
253
  submit_answer,
254
  inputs=[question_box, chunk_state, answer_box, correct_state, total_state, language_selector],