Hints and progression¶
Hints are short messages that the student can display on demand
by clicking a 💡 button in the overlay. They are designed as a
pedagogical safety net: the student solves the enigma without
them as long as they can; if they get stuck, the instructor can
authorize them. This page describes the three-level mechanism that
drives hint visibility: the hintAvailable field of each enigma,
the real-time instructor toggle, and the conditional rendering on
the React side.
The three visibility levers¶
A 💡 button only appears on an enigma if the following three conditions are satisfied simultaneously:
flowchart LR
A["enigme.hintAvailable<br/>(boolean defined in Java)"] --> AND{AND}
B["pedagogiqueHintsEnabled<br/>(instructor toggle)"] --> AND
C["enigme.hint != null<br/>(i18n-resolved text)"] --> AND
AND --> OUI[💡 Button displayed]
AND -.->|missing 1|NON[No button]
| Control level | Scope | Decided by | When |
|---|---|---|---|
Per enigma — hintAvailable |
A single step | Catalog designer (Java code) | At Enigme record definition |
Global session — pedagogiqueHintsEnabled |
All steps of the active journey | Instructor | At runtime, during the journey, via a chaos-admin button |
Client rendering — absence of hint |
A single step | i18n configuration | At React session load |
The first two levers are independent: disabling the global toggle
hides the buttons for all enigmas, including those with
hintAvailable = true. Conversely, the toggle does not make anything
appear for an enigma with hintAvailable = false.
The hintAvailable field per enigma¶
In the Enigme record, hintAvailable is a boolean defined at
construction:
new Enigme("BAC3-17", "bac3", 17,
t("bac3", "BAC3-17.text"), "right",
t("bac3", "BAC3-17.hint"),
true, // ← hintAvailable
h("…"),
t("bac3", "BAC3-17.culturalNote"),
"checkout"
);
Today, all 100 enigmas across the five levels have
hintAvailable = true. The field is kept as an extension point: a
designer could disable the hint on a specific enigma (for example a
timed final challenge in a contest) without touching the global logic.
The hint itself (enigme.hint) is resolved from the i18n JSON via
t(level, "BACx-n.hint"). If the key is absent from the file, t()
returns [BACx-n.hint] — visible on screen, which immediately flags
the missing key to the designer without preventing the enigma from
working.
The instructor toggle¶
The global toggle is held by the pedagogiqueHintsEnabled field of
ChaosStudentController, declared volatile to guarantee visibility
between threads:
It is initialized to true at application startup and reset to
true on every deactivate or level change — each journey
therefore starts with hints enabled. The instructor can toggle this
flag at any time via a dedicated endpoint.
Endpoint POST /pedagogique/hints¶
@PostMapping("/pedagogique/hints")
public ResponseEntity<?> setHintsEnabled(@RequestBody Map<String, Object> body,
HttpSession session, @RequestHeader(value = "X-Admin-Token", required = false) String tok) {
if (!AdminAuth.isAdmin(session, tok)) return unauth();
Object val = body.get("enabled");
if (!(val instanceof Boolean))
return ResponseEntity.badRequest().body(Map.of("error",
i18n.t("student.pedagogique.error.hints_enabled_required")));
pedagogiqueHintsEnabled = (Boolean) val;
log.info("[Pedagogical] Hints {} by the instructor",
pedagogiqueHintsEnabled ? "enabled" : "hidden");
return ResponseEntity.ok(Map.of("success", true,
"hintsEnabled", pedagogiqueHintsEnabled));
}
| Property | Value |
|---|---|
| Method | POST |
| URL | /api/chaos/student/pedagogique/hints |
| Authentication | X-Admin-Token header or admin session |
| Body | {"enabled": true|false} |
| Response 200 | {"success": true, "hintsEnabled": <bool>} |
| Response 400 | If enabled is absent or not a boolean |
| Response 401 | If the admin token is missing or invalid |
Body validation explicitly rejects anything that is not a boolean — a
client sending {"enabled": "true"} (string) or {"enabled": 1}
(integer) receives a 400.
Propagation to students¶
Unlike other parameters, the toggle is not pushed to students: it
is re-read on each poll in the /status response. This "pull"
strategy simplifies the architecture (no WebSocket, no SSE) and
remains reactive enough since polling runs every 15 seconds.
sequenceDiagram
autonumber
actor F as Instructor
participant ADM as chaos-admin
participant CTR as ChaosStudentController
actor E1 as Student 1
actor E2 as Student 2
Note over E1,E2: Polling in progress<br/>(15s interval)
F->>ADM: Clicks "Hide hints"
ADM->>CTR: POST /pedagogique/hints {enabled:false}
CTR->>CTR: pedagogiqueHintsEnabled = false
CTR-->>ADM: 200 {hintsEnabled:false}
Note over ADM: UI confirms "hidden"
par Next student polls
E1->>CTR: GET /status
CTR-->>E1: pedagogique.hintsEnabled = false
E1->>E1: 💡 button disappears
and
E2->>CTR: GET /status
CTR-->>E2: pedagogique.hintsEnabled = false
E2->>E2: 💡 button disappears
end
Note over F: Instructor sees that<br/>all students understood
F->>ADM: Clicks "Show hints"
ADM->>CTR: POST /pedagogique/hints {enabled:true}
CTR->>CTR: pedagogiqueHintsEnabled = true
par Next polls
E1->>CTR: GET /status
CTR-->>E1: pedagogique.hintsEnabled = true
E1->>E1: 💡 button reappears
and
E2->>CTR: GET /status
CTR-->>E2: pedagogique.hintsEnabled = true
end
Observed latency¶
The latency between the instructor's click and the disappearance of the 💡 button on the student side is at worst 15 seconds (polling interval) plus the backend response time (a few milliseconds). In practice, since the polls of several students are not synchronized with each other, the disappearance is progressive across the whole class in less than one full cycle.
This asynchrony is a pedagogical advantage: if the instructor enables hints to "unblock" a specific student, there is no immediate visible effect on the rest of the class that could disturb the others' concentration.
Conditional rendering on the React side¶
The hintsEnabled field is propagated down to PedagogiqueOverlay
through a prop:
<PedagogiqueOverlay
enigme={state.enigme}
step={state.step}
totalSteps={state.totalSteps}
onValidated={refresh}
timerExpired={timerExpired}
skipPollingRef={skipPollingRef}
hintsEnabled={state.hintsEnabled !== false}
/>
The !== false test is deliberate: it treats undefined and null
as "enabled" by default, in order to be backward-compatible with
sessions that would not yet include the field (for example during a
partial frontend/backend rollback).
The overlay renders the 💡 button only if the three conditions are met:
{enigme.hintAvailable && hintsEnabled && (
<div style={{ marginBottom: '10px' }}>
<button onClick={() => setHintVisible(v => !v)}>
💡 {hintVisible ? t('ped.hideHint') : t('ped.showHint')}
</button>
{hintVisible && enigme.hint && (
<div>{enigme.hint}</div>
)}
</div>
)}
The button shows or hides the hint by toggling a local state
(hintVisible) — the text is never masked server-side once sent,
it is purely a display state. The hintVisible state is
automatically reset to false on every step change via the
useEffect([enigme?.id]) of the overlay.
Interaction with the Logic theme¶
The final Logic & Math theme (see themes.md) has
its own hint mechanism independent of the instructor toggle. Each
question in the pool of 25 comes with a short hint, stored in the
hint field of the logique_fr.json file, and displayed via a
question-local 💡 button. The global pedagogiqueHintsEnabled toggle
does not affect this page.
The reason is functional: the success page and the final theme are displayed after the end of the main journey. At this stage, the notion of "class in progress" has lost its meaning — the student is on their end screen, the instructor is no longer driving anything. Cutting off access to the final theme hints would be a false signal.
Relevant i18n keys¶
To translate or adapt hint-related messages, target:
| Key | File | Usage |
|---|---|---|
ped.showHint |
frontend/src/i18n/fr.json and en.json |
Button label when the hint is hidden |
ped.hideHint |
frontend/src/i18n/fr.json and en.json |
Label when the hint is visible |
BACx-n.hint |
backend/src/main/resources/i18n/enigmes/bacX/enigmes_XX.json |
Text of each individual hint |
student.pedagogique.error.hints_enabled_required |
messages_fr.properties and messages_en.properties |
400 error if body is malformed |
Next pages: ← Star system · Success page → · Final themes →