Fix CBT history react-markdown crash, fix test deletion cascade, fix cbt theme, add 180 NEET questions

This commit is contained in:
2026-07-09 23:37:15 +05:30
parent 7c5acbd823
commit 149d365218
11 changed files with 8308 additions and 64 deletions

View File

@@ -628,8 +628,6 @@ def create_manual_test(
created_by=data.admin_id created_by=data.admin_id
) )
session.add(test_obj) session.add(test_obj)
session.commit()
session.refresh(test_obj)
# 2. Parse and create questions # 2. Parse and create questions
sections_dict = {} sections_dict = {}
@@ -649,8 +647,6 @@ def create_manual_test(
created_by=data.admin_id created_by=data.admin_id
) )
session.add(q_obj) session.add(q_obj)
session.commit()
session.refresh(q_obj)
sec_name = q_data.section sec_name = q_data.section
if sec_name not in sections_dict: if sec_name not in sections_dict:
@@ -679,3 +675,68 @@ def toggle_test_status(test_id: UUID, session: Session = Depends(get_session)):
session.commit() session.commit()
return {"status": "success", "is_active": test_obj.is_active} return {"status": "success", "is_active": test_obj.is_active}
@app.delete("/api/admin/tests/{test_id}", tags=["Admin"])
def delete_test(test_id: UUID, session: Session = Depends(get_session)):
from sqlmodel import select
test_obj = session.get(models.Test, test_id)
if not test_obj:
raise HTTPException(status_code=404, detail="Test not found")
# 1. Identify all question IDs associated with the test
question_ids = []
if test_obj.sections:
for section_name, section_data in test_obj.sections.items():
question_ids.extend(section_data.get("questions", []))
# Convert question IDs to UUIDs
import uuid
q_uuids = []
for q_id in question_ids:
try:
q_uuids.append(uuid.UUID(q_id) if isinstance(q_id, str) else q_id)
except ValueError:
continue
# 2. Identify all attempts associated with this test
attempts_query = select(models.Attempt).where(
models.Attempt.source_id == test_id
)
attempts = session.exec(attempts_query).all()
attempt_ids = [att.id for att in attempts]
# 3. Delete Mistake records referencing these attempts
if attempt_ids:
mistakes_by_attempt_query = select(models.Mistake).where(
models.Mistake.attempt_id.in_(attempt_ids)
)
mistakes_by_attempt = session.exec(mistakes_by_attempt_query).all()
for mistake in mistakes_by_attempt:
session.delete(mistake)
# 4. Delete Mistake records referencing these questions
if q_uuids:
mistakes_by_q_query = select(models.Mistake).where(
models.Mistake.question_id.in_(q_uuids)
)
mistakes_by_q = session.exec(mistakes_by_q_query).all()
for mistake in mistakes_by_q:
try:
session.delete(mistake)
except Exception:
pass
# 5. Delete the Attempt records
for att in attempts:
session.delete(att)
# 6. Delete the Question records
for q_uuid in q_uuids:
q_obj = session.get(models.Question, q_uuid)
if q_obj:
session.delete(q_obj)
# 7. Delete the Test itself
session.delete(test_obj)
session.commit()
return {"status": "success", "message": "Test and all associated questions, attempts, and mistakes deleted successfully"}

1832
jee_advanced_full_90q.json Normal file

File diff suppressed because it is too large Load Diff

3782
neet_180_questions.json Normal file

File diff suppressed because it is too large Load Diff

479
neet_sample_questions.md Normal file
View File

@@ -0,0 +1,479 @@
# NEET Sample Questions
Here are some realistic sample questions for a NEET exam.
### Q1. The dimension of $\frac{1}{\sqrt{\mu_0 \varepsilon_0}}$ is that of:
**Subject**: Physics | **Topic**: Units and Measurements | **Difficulty**: Medium
- **A**: Velocity
- **B**: Time
- **C**: Capacitance
- **D**: Distance
**Correct Answer**: A
**Solution**:
We know that $c = \frac{1}{\sqrt{\mu_0 \varepsilon_0}}$, where $c$ is the speed of light in vacuum. Therefore, the dimension of $\frac{1}{\sqrt{\mu_0 \varepsilon_0}}$ is that of velocity, which is $[L T^{-1}]$.
---
### Q2. Two projectiles are thrown with the same initial velocity at angles of $30^\circ$ and $60^\circ$ with the horizontal. The ratio of their maximum heights is:
**Subject**: Physics | **Topic**: Kinematics | **Difficulty**: Easy
- **A**: 1:3
- **B**: 3:1
- **C**: 1:\sqrt{3}
- **D**: 1:2
**Correct Answer**: A
**Solution**:
Maximum height $H = \frac{u^2 \sin^2 \theta}{2g}$.
For $\theta_1 = 30^\circ$, $H_1 = \frac{u^2 (1/2)^2}{2g} = \frac{u^2}{8g}$.
For $\theta_2 = 60^\circ$, $H_2 = \frac{u^2 (\sqrt{3}/2)^2}{2g} = \frac{3u^2}{8g}$.
Ratio $H_1 : H_2 = 1 : 3$.
---
### Q3. A Carnot engine takes heat from a reservoir at $627^\circ\text{C}$ and rejects heat to a sink at $27^\circ\text{C}$. Its efficiency is:
**Subject**: Physics | **Topic**: Thermodynamics | **Difficulty**: Medium
- **A**: 2/3
- **B**: 1/3
- **C**: 3/4
- **D**: 1/2
**Correct Answer**: A
**Solution**:
Temperature of source, $T_1 = 627 + 273 = 900\text{ K}$.
Temperature of sink, $T_2 = 27 + 273 = 300\text{ K}$.
Efficiency $\eta = 1 - \frac{T_2}{T_1} = 1 - \frac{300}{900} = 1 - \frac{1}{3} = \frac{2}{3}$.
---
### Q4. A parallel plate capacitor with air between the plates has a capacitance of $8\text{ pF}$. What will be the capacitance if the distance between the plates is reduced by half, and the space between them is filled with a substance of dielectric constant $6$?
**Subject**: Physics | **Topic**: Electrostatics | **Difficulty**: Hard
- **A**: 96 pF
- **B**: 8 pF
- **C**: 48 pF
- **D**: 12 pF
**Correct Answer**: A
**Solution**:
Initial capacitance $C_0 = \frac{\varepsilon_0 A}{d} = 8\text{ pF}$.
New distance $d' = d/2$ and new dielectric constant $K = 6$.
New capacitance $C' = \frac{K \varepsilon_0 A}{d'} = \frac{6 \varepsilon_0 A}{d/2} = 12 \left(\frac{\varepsilon_0 A}{d}\right) = 12 \times 8\text{ pF} = 96\text{ pF}$.
---
### Q5. In a meter bridge experiment, the null point is obtained at $20\text{ cm}$ from one end of the wire when resistance $X$ is balanced against another resistance $Y$. If $X < Y$, then what will be the new position of the null point from the same end, if one decides to balance a resistance of $4X$ against $Y$?
**Subject**: Physics | **Topic**: Current Electricity | **Difficulty**: Medium
- **A**: 50 cm
- **B**: 40 cm
- **C**: 80 cm
- **D**: 20 cm
**Correct Answer**: A
**Solution**:
Initially, $\frac{X}{Y} = \frac{20}{100-20} = \frac{20}{80} = \frac{1}{4}$. So $Y = 4X$.
For new resistance $4X$ against $Y$, ratio is $\frac{4X}{Y} = \frac{4X}{4X} = 1$.
Let new null point be $l$, then $\frac{l}{100-l} = 1 \implies l = 100 - l \implies 2l = 100 \implies l = 50\text{ cm}$.
---
### Q6. The magnetic field at the centre of a circular current carrying coil of radius $R$ is $B_0$. If the radius is doubled keeping the current same, the new magnetic field at the centre will be:
**Subject**: Physics | **Topic**: Magnetism | **Difficulty**: Easy
- **A**: $B_0/2$
- **B**: $B_0/4$
- **C**: $2B_0$
- **D**: $B_0$
**Correct Answer**: A
**Solution**:
Magnetic field at the centre of a circular coil is $B = \frac{\mu_0 I}{2R}$.
Since $B \propto \frac{1}{R}$, doubling the radius will make the magnetic field half. Thus, new field is $B_0/2$.
---
### Q7. In a series LCR circuit, the voltage across the resistance, capacitance, and inductance is $10\text{ V}$ each. If the capacitance is short-circuited, the voltage across the inductance will be:
**Subject**: Physics | **Topic**: Alternating Current | **Difficulty**: Medium
- **A**: 10 V
- **B**: $10\sqrt{2}\text{ V}$
- **C**: $10/\sqrt{2}\text{ V}$
- **D**: Zero
**Correct Answer**: C
**Solution**:
Initially, $V_R = 10\text{ V}$, $V_L = 10\text{ V}$, $V_C = 10\text{ V}$. Since $V_L = V_C$, the circuit is in resonance. The applied voltage $V = \sqrt{V_R^2 + (V_L - V_C)^2} = V_R = 10\text{ V}$.
If capacitance is shorted, it becomes an LR circuit. Impedance $Z = \sqrt{R^2 + X_L^2}$. Since $V_R = V_L$ initially, $R = X_L$, so $Z = R\sqrt{2}$.
New current $I' = \frac{V}{Z} = \frac{10}{R\sqrt{2}}$.
New voltage across inductance $V_L' = I' X_L = \frac{10}{R\sqrt{2}} \times R = \frac{10}{\sqrt{2}}\text{ V}$.
---
### Q8. In Young's double slit experiment, if the separation between coherent sources is halved and the distance of the screen from the coherent sources is doubled, then the fringe width becomes:
**Subject**: Physics | **Topic**: Optics | **Difficulty**: Medium
- **A**: Half
- **B**: Four times
- **C**: One-fourth
- **D**: Double
**Correct Answer**: B
**Solution**:
Fringe width $\beta = \frac{\lambda D}{d}$.
New slit separation $d' = d/2$.
New screen distance $D' = 2D$.
New fringe width $\beta' = \frac{\lambda (2D)}{(d/2)} = 4 \frac{\lambda D}{d} = 4\beta$.
---
### Q9. When the light of frequency $2\nu_0$ (where $\nu_0$ is threshold frequency) is incident on a metal plate, the maximum velocity of electrons emitted is $v_1$. When the frequency of the incident radiation is increased to $5\nu_0$, the maximum velocity of electrons emitted from the same plate is $v_2$. The ratio of $v_1$ to $v_2$ is:
**Subject**: Physics | **Topic**: Modern Physics | **Difficulty**: Hard
- **A**: 1:2
- **B**: 1:4
- **C**: 4:1
- **D**: 2:1
**Correct Answer**: A
**Solution**:
From Einstein's photoelectric equation: $K_{\max} = h\nu - h\nu_0 = \frac{1}{2} m v^2$.
Case 1: $\frac{1}{2} m v_1^2 = h(2\nu_0) - h\nu_0 = h\nu_0$.
Case 2: $\frac{1}{2} m v_2^2 = h(5\nu_0) - h\nu_0 = 4h\nu_0$.
Dividing the two equations: $\frac{v_1^2}{v_2^2} = \frac{1}{4} \implies \frac{v_1}{v_2} = \frac{1}{2}$.
---
### Q10. Which logic gate is represented by the boolean expression $Y = \overline{A \cdot B}$?
**Subject**: Physics | **Topic**: Semiconductors | **Difficulty**: Easy
- **A**: NOR
- **B**: OR
- **C**: NAND
- **D**: AND
**Correct Answer**: C
**Solution**:
The boolean expression $Y = \overline{A \cdot B}$ represents the NAND gate. It gives an output of 0 only when both inputs A and B are 1, and 1 otherwise.
---
### Q11. The number of water molecules is maximum in:
**Subject**: Chemistry | **Topic**: Physical Chemistry | **Difficulty**: Easy
- **A**: 18 grams of water
- **B**: 18 moles of water
- **C**: 18 molecules of water
- **D**: 1.8 grams of water
**Correct Answer**: B
**Solution**:
18 grams of water = 1 mole = $N_A$ molecules.
18 moles of water = $18 N_A$ molecules.
1.8 grams of water = 0.1 mole = $0.1 N_A$ molecules.
Thus, 18 moles of water contains the maximum number of molecules.
---
### Q12. The maximum number of electrons in an atom that can have the quantum numbers $n = 4$ and $m_l = +1$ is:
**Subject**: Chemistry | **Topic**: Physical Chemistry | **Difficulty**: Medium
- **A**: 2
- **B**: 4
- **C**: 6
- **D**: 8
**Correct Answer**: C
**Solution**:
For $n = 4$, the possible values of $l$ are 0, 1, 2, 3.
The condition $m_l = +1$ is possible for $l = 1$, $l = 2$, and $l = 3$.
Each subshell ($l=1, 2, 3$) will have one orbital with $m_l = +1$.
Since each orbital can hold 2 electrons, the total number of electrons is $3 \times 2 = 6$.
---
### Q13. The species having pyramidal shape is:
**Subject**: Chemistry | **Topic**: Inorganic Chemistry | **Difficulty**: Medium
- **A**: $\text{SO}_3$
- **B**: $\text{BrF}_3$
- **C**: $\text{SiO}_3^{2-}$
- **D**: $\text{OSF}_2$
**Correct Answer**: D
**Solution**:
In $\text{OSF}_2$, the central sulfur atom has 4 electron domains (1 double bond to O, 2 single bonds to F, and 1 lone pair). This corresponds to an $sp^3$ hybridization with a trigonal pyramidal shape. $\text{SO}_3$ is trigonal planar, $\text{BrF}_3$ is T-shaped.
---
### Q14. For the reversible reaction $\text{N}_2(\text{g}) + 3\text{H}_2(\text{g}) \rightleftharpoons 2\text{NH}_3(\text{g}) + \text{Heat}$, the equilibrium shifts in forward direction:
**Subject**: Chemistry | **Topic**: Physical Chemistry | **Difficulty**: Medium
- **A**: By increasing the concentration of $\text{NH}_3(\text{g})$
- **B**: By decreasing the pressure
- **C**: By decreasing the concentrations of $\text{N}_2(\text{g})$ and $\text{H}_2(\text{g})$
- **D**: By increasing pressure and decreasing temperature
**Correct Answer**: D
**Solution**:
According to Le Chatelier's principle, an exothermic reaction ($\Delta H < 0$) is favored by low temperatures. A reaction involving a decrease in the number of gaseous moles ($4 \rightarrow 2$) is favored by high pressures. Therefore, increasing pressure and decreasing temperature will shift the equilibrium in the forward direction.
---
### Q15. Given $E^\circ_{\text{Cr}^{3+}/\text{Cr}} = -0.74\text{ V}$; $E^\circ_{\text{MnO}_4^-/\text{Mn}^{2+}} = 1.51\text{ V}$; $E^\circ_{\text{Cr}_2\text{O}_7^{2-}/\text{Cr}^{3+}} = 1.33\text{ V}$; $E^\circ_{\text{Cl}/\text{Cl}^-} = 1.36\text{ V}$.
Based on the data given above, strongest oxidizing agent will be:
**Subject**: Chemistry | **Topic**: Physical Chemistry | **Difficulty**: Hard
- **A**: $\text{Cl}$
- **B**: $\text{Cr}^{3+}$
- **C**: $\text{Mn}^{2+}$
- **D**: $\text{MnO}_4^-$
**Correct Answer**: D
**Solution**:
Higher the standard reduction potential ($E^\circ$), stronger is the oxidizing power. The $E^\circ$ value for $\text{MnO}_4^-/\text{Mn}^{2+}$ is the highest ($+1.51\text{ V}$). Therefore, $\text{MnO}_4^-$ is the strongest oxidizing agent among the given species.
---
### Q16. Which of the following carbocations is most stable?
**Subject**: Chemistry | **Topic**: Organic Chemistry | **Difficulty**: Medium
- **A**: Allyl carbocation
- **B**: Primary alkyl carbocation
- **C**: Tert-butyl carbocation
- **D**: Triphenylmethyl carbocation
**Correct Answer**: D
**Solution**:
Stability of carbocations is determined by delocalization of charge. The triphenylmethyl carbocation, $(\text{C}_6\text{H}_5)_3\text{C}^+$, has 3 phenyl rings providing extensive resonance stabilization, making it exceptionally stable compared to the others.
---
### Q17. Ozonolysis of an organic compound gives formaldehyde as one of the products. This confirms the presence of:
**Subject**: Chemistry | **Topic**: Organic Chemistry | **Difficulty**: Hard
- **A**: Two isomeric double bonds
- **B**: A vinyl group
- **C**: An isopropyl group
- **D**: An acetylenic triple bond
**Correct Answer**: B
**Solution**:
Ozonolysis cleaves a carbon-carbon double bond and adds oxygen. Formaldehyde ($\text{HCHO}$) is produced if the original double bond was at the end of a chain, specifically involving a terminal $=\text{CH}_2$ group, which is characteristic of a vinyl group ($-\text{CH}=\text{CH}_2$).
---
### Q18. The IUPAC name of $[\text{Pt}(\text{NH}_3)_2\text{Cl}(\text{NO}_2)]$ is:
**Subject**: Chemistry | **Topic**: Inorganic Chemistry | **Difficulty**: Medium
- **A**: Platinum diaminechloronitrite
- **B**: Chloronitrito-N-ammineplatinum(II)
- **C**: Diamminechloridonitrito-N-platinum(II)
- **D**: Diamminechloronitrito-O-platinate(II)
**Correct Answer**: C
**Solution**:
Ligands are named in alphabetical order: ammine, chlorido, nitrito-N (since it coordinates through nitrogen). The oxidation state of Pt is $x + 0 + (-1) + (-1) = 0 \implies x = +2$. Thus, the correct name is Diamminechloridonitrito-N-platinum(II).
---
### Q19. The unit of rate constant for a zero order reaction is:
**Subject**: Chemistry | **Topic**: Physical Chemistry | **Difficulty**: Easy
- **A**: $\text{s}^{-1}$
- **B**: $\text{mol}\cdot\text{L}^{-1}\cdot\text{s}^{-1}$
- **C**: $\text{L}\cdot\text{mol}^{-1}\cdot\text{s}^{-1}$
- **D**: $\text{L}^2\cdot\text{mol}^{-2}\cdot\text{s}^{-1}$
**Correct Answer**: B
**Solution**:
For a zero-order reaction, Rate = $k[A]^0 = k$. The unit of rate is concentration per unit time ($\text{mol}\cdot\text{L}^{-1}\cdot\text{s}^{-1}$). Thus, the unit of the rate constant $k$ is also $\text{mol}\cdot\text{L}^{-1}\cdot\text{s}^{-1}$.
---
### Q20. Which of the following hormones is produced under the condition of stress which stimulates glycogenolysis in the liver of human beings?
**Subject**: Chemistry | **Topic**: Organic Chemistry | **Difficulty**: Medium
- **A**: Thyroxin
- **B**: Insulin
- **C**: Adrenaline
- **D**: Estradiol
**Correct Answer**: C
**Solution**:
Adrenaline (epinephrine) is the 'fight or flight' hormone released during stress. It promotes the breakdown of glycogen into glucose (glycogenolysis) in the liver to increase blood glucose levels for energy.
---
### Q21. Crossing over occurs during which stage of meiosis?
**Subject**: Biology | **Topic**: Cell Structure and Function | **Difficulty**: Medium
- **A**: Leptotene
- **B**: Pachytene
- **C**: Diplotene
- **D**: Zygotene
**Correct Answer**: B
**Solution**:
Crossing over, the exchange of genetic material between non-sister chromatids of homologous chromosomes, occurs during the pachytene stage of prophase I of meiosis.
---
### Q22. A cross between two tall plants resulted in offspring having few dwarf plants. What would be the genotypes of both the parents?
**Subject**: Biology | **Topic**: Genetics and Evolution | **Difficulty**: Easy
- **A**: TT and Tt
- **B**: Tt and Tt
- **C**: TT and TT
- **D**: Tt and tt
**Correct Answer**: B
**Solution**:
Since both parents are tall but they produce some dwarf offspring, both parents must carry the recessive allele for dwarfness. Thus, their genotype must be heterozygous (Tt). A cross of Tt x Tt yields a 3:1 ratio of tall to dwarf.
---
### Q23. Which of the following RNAs should be most abundant in an animal cell?
**Subject**: Biology | **Topic**: Genetics and Evolution | **Difficulty**: Medium
- **A**: tRNA
- **B**: mRNA
- **C**: miRNA
- **D**: rRNA
**Correct Answer**: D
**Solution**:
Ribosomal RNA (rRNA) constitutes about 80% of the total RNA in an actively growing cell. It forms the structural and catalytic core of ribosomes.
---
### Q24. Which of the following terms describes human dentition?
**Subject**: Biology | **Topic**: Human Physiology | **Difficulty**: Hard
- **A**: Thecodont, Diphyodont, Homodont
- **B**: Thecodont, Diphyodont, Heterodont
- **C**: Pleurodont, Monophyodont, Homodont
- **D**: Pleurodont, Diphyodont, Heterodont
**Correct Answer**: B
**Solution**:
Human dentition is Thecodont (teeth embedded in jaw sockets), Diphyodont (two sets of teeth: deciduous and permanent), and Heterodont (different types of teeth: incisors, canines, premolars, molars).
---
### Q25. Which hormone is responsible for the ovulation in human females?
**Subject**: Biology | **Topic**: Reproduction | **Difficulty**: Medium
- **A**: FSH (Follicle Stimulating Hormone)
- **B**: Progesterone
- **C**: LH (Luteinizing Hormone)
- **D**: Estrogen
**Correct Answer**: C
**Solution**:
A surge in LH (Luteinizing Hormone) mid-cycle triggers the rupture of the Graafian follicle and the subsequent release of the ovum, a process known as ovulation.
---
### Q26. The first stable product of CO2 fixation in C3 plants is:
**Subject**: Biology | **Topic**: Plant Physiology | **Difficulty**: Medium
- **A**: Phosphoglyceraldehyde (PGAL)
- **B**: Malic acid
- **C**: Oxaloacetic acid (OAA)
- **D**: 3-Phosphoglyceric acid (3-PGA)
**Correct Answer**: D
**Solution**:
In the Calvin cycle of C3 plants, CO2 combines with RuBP to form a 6-carbon unstable intermediate that immediately splits into two molecules of 3-phosphoglyceric acid (3-PGA), the first stable product.
---
### Q27. The pyramid of energy is always:
**Subject**: Biology | **Topic**: Ecology | **Difficulty**: Easy
- **A**: Upright
- **B**: Inverted
- **C**: Spindle shaped
- **D**: Variable
**Correct Answer**: A
**Solution**:
The pyramid of energy is always upright because energy is lost as heat at each trophic level (10% law), so energy always decreases as it flows from producers to higher trophic levels.
---
### Q28. The cutting of DNA at specific locations became possible with the discovery of:
**Subject**: Biology | **Topic**: Biotechnology | **Difficulty**: Medium
- **A**: Selectable markers
- **B**: Ligases
- **C**: Restriction enzymes
- **D**: Probes
**Correct Answer**: C
**Solution**:
Restriction endonucleases (or restriction enzymes) cut DNA at specific recognition sequences, making recombinant DNA technology possible.
---
### Q29. Receptor sites for neurotransmitters are present on:
**Subject**: Biology | **Topic**: Human Physiology | **Difficulty**: Hard
- **A**: Pre-synaptic membrane
- **B**: Tips of axons
- **C**: Post-synaptic membrane
- **D**: Membrane of synaptic vesicles
**Correct Answer**: C
**Solution**:
Neurotransmitters are released from synaptic vesicles in the pre-synaptic neuron and diffuse across the synaptic cleft to bind with specific receptor sites located on the post-synaptic membrane.
---
### Q30. Which of the following are found in extreme saline conditions?
**Subject**: Biology | **Topic**: Diversity in Living World | **Difficulty**: Medium
- **A**: Eubacteria
- **B**: Cyanobacteria
- **C**: Mycobacteria
- **D**: Archaebacteria
**Correct Answer**: D
**Solution**:
Archaebacteria are known to live in some of the most harsh habitats. Specifically, halophiles live in extreme saline conditions (e.g., Dead Sea).
---

13
package-lock.json generated
View File

@@ -17,6 +17,7 @@
"katex": "^0.17.0", "katex": "^0.17.0",
"lucide-react": "^1.21.0", "lucide-react": "^1.21.0",
"next": "16.2.9", "next": "16.2.9",
"next-themes": "^0.4.6",
"react": "19.2.4", "react": "19.2.4",
"react-datepicker": "^9.1.0", "react-datepicker": "^9.1.0",
"react-dom": "19.2.4", "react-dom": "19.2.4",
@@ -27,6 +28,7 @@
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
"tailwind-merge": "^3.6.0", "tailwind-merge": "^3.6.0",
"zod": "^4.4.3",
"zustand": "^5.0.14" "zustand": "^5.0.14"
}, },
"devDependencies": { "devDependencies": {
@@ -6754,6 +6756,16 @@
} }
} }
}, },
"node_modules/next-themes": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
"integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/next/node_modules/postcss": { "node_modules/next/node_modules/postcss": {
"version": "8.4.31", "version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@@ -8888,7 +8900,6 @@
"version": "4.4.3", "version": "4.4.3",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz",
"integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==",
"dev": true,
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"

View File

@@ -18,6 +18,7 @@
"katex": "^0.17.0", "katex": "^0.17.0",
"lucide-react": "^1.21.0", "lucide-react": "^1.21.0",
"next": "16.2.9", "next": "16.2.9",
"next-themes": "^0.4.6",
"react": "19.2.4", "react": "19.2.4",
"react-datepicker": "^9.1.0", "react-datepicker": "^9.1.0",
"react-dom": "19.2.4", "react-dom": "19.2.4",
@@ -28,6 +29,7 @@
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"remark-math": "^6.0.0", "remark-math": "^6.0.0",
"tailwind-merge": "^3.6.0", "tailwind-merge": "^3.6.0",
"zod": "^4.4.3",
"zustand": "^5.0.14" "zustand": "^5.0.14"
}, },
"devDependencies": { "devDependencies": {

File diff suppressed because it is too large Load Diff

View File

@@ -6,24 +6,51 @@ import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Switch } from "@/components/ui/switch" import { Switch } from "@/components/ui/switch"
import { Input } from "@/components/ui/input" import { Input } from "@/components/ui/input"
import { Loader2, Upload, FileText, Clock, CalendarIcon } from "lucide-react" import { Loader2, Upload, FileText, Clock, CalendarIcon, CheckCircle2, AlertCircle, X, Trash2 } from "lucide-react"
import DatePicker from "react-datepicker" import DatePicker from "react-datepicker"
import "react-datepicker/dist/react-datepicker.css" import "react-datepicker/dist/react-datepicker.css"
import { z } from "zod"
import { useActiveUser } from "@/hooks/useActiveUser" import { useActiveUser } from "@/hooks/useActiveUser"
// Strict Zod schema for client-side validation
const QuestionSchema = z.object({
id: z.string().optional(),
section: z.string().default("General"),
subject: z.string(),
topic: z.string(),
subtopic: z.string(),
difficulty: z.string(),
type: z.enum(["MCQ_Single", "MCQ_Multiple", "Numerical"]),
question_text: z.string(),
options: z.record(z.string(), z.any()).nullable().optional(),
correct_answer: z.object({
value: z.any()
}),
solution_text: z.string(),
language: z.enum(["English", "Hindi", "Bilingual"])
}).passthrough();
const TestSchema = z.array(QuestionSchema);
export default function AdminTestsPage() { export default function AdminTestsPage() {
const [tests, setTests] = useState<any[]>([]) const [tests, setTests] = useState<any[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [uploading, setUploading] = useState(false) const [uploading, setUploading] = useState(false)
const { userId } = useActiveUser() const { userId } = useActiveUser()
// Upload Form State // Upload Form State
const [title, setTitle] = useState("") const [title, setTitle] = useState("")
const [examPattern, setExamPattern] = useState("JEE_Main") const [examPattern, setExamPattern] = useState("JEE_Main")
const [timeLimit, setTimeLimit] = useState("180") const [timeLimit, setTimeLimit] = useState("180")
const [scheduledDate, setScheduledDate] = useState<Date | null>(null) const [scheduledDate, setScheduledDate] = useState<Date | null>(null)
const [endDate, setEndDate] = useState<Date | null>(null) const [endDate, setEndDate] = useState<Date | null>(null)
const [questionsJson, setQuestionsJson] = useState("")
// Drag and Drop State
const [isDragging, setIsDragging] = useState(false)
const [validQuestions, setValidQuestions] = useState<Record<string, unknown>[] | null>(null)
const [validationErrors, setValidationErrors] = useState<{ path: string, message: string }[]>([])
const fileInputRef = useRef<HTMLInputElement>(null)
const fetchTests = async () => { const fetchTests = async () => {
try { try {
@@ -40,6 +67,7 @@ export default function AdminTestsPage() {
} }
useEffect(() => { useEffect(() => {
// eslint-disable-next-line react-hooks/set-state-in-effect
fetchTests() fetchTests()
}, []) }, [])
@@ -56,28 +84,106 @@ export default function AdminTestsPage() {
} }
} }
const handleDelete = async (testId: string) => {
if (!confirm("Are you sure you want to delete this test and all its questions? This action cannot be undone.")) return
try {
const res = await fetch(`/api/admin/tests/${testId}`, {
method: "DELETE"
})
if (res.ok) {
alert("Test deleted successfully!")
fetchTests()
} else {
alert("Failed to delete test.")
}
} catch (e) {
console.error(e)
alert("Error deleting test")
}
}
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault()
setIsDragging(true)
}
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault()
setIsDragging(false)
}
const handleDrop = (e: React.DragEvent) => {
e.preventDefault()
setIsDragging(false)
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
processFile(e.dataTransfer.files[0])
}
}
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files && e.target.files.length > 0) {
processFile(e.target.files[0])
}
}
const processFile = (file: File) => {
if (!file.name.endsWith('.json')) {
setValidationErrors([{ path: "File", message: "Only .json files are allowed." }])
setValidQuestions(null)
return
}
const reader = new FileReader()
reader.onload = (e) => {
try {
const json = JSON.parse(e.target?.result as string)
// Zod validation
const result = TestSchema.safeParse(json)
if (result.success) {
setValidQuestions(result.data)
setValidationErrors([])
} else {
setValidQuestions(null)
const errors = (result as any).error.errors.map((err: any) => ({
path: err.path.join('.'),
message: err.message
}))
setValidationErrors(errors)
}
} catch {
setValidQuestions(null)
setValidationErrors([{ path: "JSON Parsing", message: "Invalid JSON format. Check for syntax errors." }])
}
}
reader.readAsText(file)
}
const removeFile = () => {
setValidQuestions(null)
setValidationErrors([])
if (fileInputRef.current) {
fileInputRef.current.value = ""
}
}
const handleUpload = async (e: React.FormEvent) => { const handleUpload = async (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
if (!questionsJson) return if (!validQuestions || validQuestions.length === 0) {
alert("Please upload a valid JSON file with questions first.")
return
}
if (!userId) return if (!userId) return
setUploading(true) setUploading(true)
let parsedQuestions = [];
try {
parsedQuestions = JSON.parse(questionsJson);
} catch(err) {
alert("Invalid JSON format. Please paste a valid JSON array.");
setUploading(false);
return;
}
const payload = { const payload = {
title, title,
exam_pattern: examPattern, exam_pattern: examPattern,
time_limit: parseInt(timeLimit), time_limit: parseInt(timeLimit),
admin_id: userId, admin_id: userId,
questions_json: parsedQuestions, questions_json: validQuestions,
scheduled_date: scheduledDate ? scheduledDate.toISOString() : null, scheduled_date: scheduledDate ? scheduledDate.toISOString() : null,
end_date: endDate ? endDate.toISOString() : null, end_date: endDate ? endDate.toISOString() : null,
}; };
@@ -92,10 +198,11 @@ export default function AdminTestsPage() {
if (res.ok) { if (res.ok) {
alert("Test created successfully!") alert("Test created successfully!")
setTitle("") setTitle("")
setQuestionsJson("") removeFile()
fetchTests() fetchTests()
} else { } else {
alert("Creation failed") const err = await res.json()
alert(`Creation failed: ${JSON.stringify(err)}`)
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@@ -115,13 +222,13 @@ export default function AdminTestsPage() {
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Upload Form */} {/* Upload Form */}
<Card className="lg:col-span-1 border-muted bg-card shadow-sm"> <Card className="lg:col-span-1 border-muted bg-card shadow-sm h-fit">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<Upload className="w-5 h-5 text-primary" /> <Upload className="w-5 h-5 text-primary" />
Create Manual Test Create Manual Test
</CardTitle> </CardTitle>
<CardDescription>Paste the structured JSON block of questions in LaTeX format to create a deterministic test.</CardDescription> <CardDescription>Upload a structured JSON array of questions in LaTeX format to create a deterministic test.</CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<form onSubmit={handleUpload} className="space-y-4"> <form onSubmit={handleUpload} className="space-y-4">
@@ -150,7 +257,7 @@ export default function AdminTestsPage() {
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="space-y-2 flex flex-col"> <div className="space-y-2 flex flex-col">
<label className="text-sm font-medium">Scheduled Start (Optional)</label> <label className="text-sm font-medium">Scheduled Start</label>
<DatePicker <DatePicker
selected={scheduledDate} selected={scheduledDate}
onChange={(date: Date | null) => setScheduledDate(date)} onChange={(date: Date | null) => setScheduledDate(date)}
@@ -160,11 +267,11 @@ export default function AdminTestsPage() {
timeCaption="time" timeCaption="time"
dateFormat="MMMM d, yyyy h:mm aa" dateFormat="MMMM d, yyyy h:mm aa"
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2" className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
placeholderText="Select start date & time" placeholderText="Optional"
/> />
</div> </div>
<div className="space-y-2 flex flex-col"> <div className="space-y-2 flex flex-col">
<label className="text-sm font-medium">Scheduled End (Optional)</label> <label className="text-sm font-medium">Scheduled End</label>
<DatePicker <DatePicker
selected={endDate} selected={endDate}
onChange={(date: Date | null) => setEndDate(date)} onChange={(date: Date | null) => setEndDate(date)}
@@ -174,24 +281,66 @@ export default function AdminTestsPage() {
timeCaption="time" timeCaption="time"
dateFormat="MMMM d, yyyy h:mm aa" dateFormat="MMMM d, yyyy h:mm aa"
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2" className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
placeholderText="Select end date & time" placeholderText="Optional"
/> />
</div> </div>
</div> </div>
<div className="space-y-2"> <div className="space-y-2 pt-2">
<label className="text-sm font-medium">Questions JSON (LaTeX Format)</label> <label className="text-sm font-medium">Questions JSON File</label>
<textarea
required {validQuestions ? (
value={questionsJson} <div className="flex items-center justify-between p-3 border border-green-500/50 bg-green-500/10 rounded-md">
onChange={e => setQuestionsJson(e.target.value)} <div className="flex items-center gap-2 text-green-600 dark:text-green-400">
className="w-full min-h-[250px] p-3 text-sm font-mono border rounded-md bg-background focus:ring-primary focus:border-primary" <CheckCircle2 className="w-5 h-5" />
placeholder='[{"section": "Physics", "text_en": "Question text...", "options_en": ["A", "B", "C", "D"], "correct_answer": "A"}]' <span className="text-sm font-medium">{validQuestions.length} questions validated</span>
</div>
<Button type="button" variant="ghost" size="icon" className="h-8 w-8 text-muted-foreground hover:text-foreground" onClick={removeFile}>
<X className="w-4 h-4" />
</Button>
</div>
) : (
<div
className={`relative flex flex-col items-center justify-center p-6 border-2 border-dashed rounded-md transition-colors ${isDragging ? "border-primary bg-primary/5" : "border-muted-foreground/25 hover:border-primary/50"}`}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onDrop={handleDrop}
>
<Upload className="w-8 h-8 text-muted-foreground mb-3" />
<p className="text-sm font-medium text-center">Drag and drop your .json file here</p>
<p className="text-xs text-muted-foreground text-center mt-1">or click to browse files</p>
<input
type="file"
accept=".json"
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
onChange={handleFileChange}
ref={fileInputRef}
/> />
<p className="text-xs text-muted-foreground mt-1">Paste your questions as a JSON array.</p> </div>
)}
{validationErrors.length > 0 && (
<div className="mt-2 p-3 border border-destructive/50 bg-destructive/10 rounded-md max-h-48 overflow-y-auto">
<div className="flex items-center gap-2 text-destructive mb-2">
<AlertCircle className="w-4 h-4" />
<span className="text-sm font-bold">Validation Errors Found</span>
</div>
<ul className="space-y-1">
{validationErrors.slice(0, 50).map((err, i) => (
<li key={i} className="text-xs text-destructive/90 flex gap-2">
<span className="font-mono bg-destructive/20 px-1 rounded">{err.path}</span>
<span>{err.message}</span>
</li>
))}
{validationErrors.length > 50 && (
<li className="text-xs text-muted-foreground italic mt-2">...and {validationErrors.length - 50} more errors</li>
)}
</ul>
</div>
)}
</div> </div>
<Button type="submit" className="w-full" disabled={uploading}> <Button type="submit" className="w-full" disabled={uploading || !validQuestions}>
{uploading ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <FileText className="w-4 h-4 mr-2" />} {uploading ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <FileText className="w-4 h-4 mr-2" />}
{uploading ? "Creating Test..." : "Create Test from JSON"} {uploading ? "Creating Test..." : "Create Test from JSON"}
</Button> </Button>
@@ -200,7 +349,7 @@ export default function AdminTestsPage() {
</Card> </Card>
{/* Existing Tests List */} {/* Existing Tests List */}
<Card className="lg:col-span-2 border-muted bg-card shadow-sm"> <Card className="lg:col-span-2 border-muted bg-card shadow-sm h-fit">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<FileText className="w-5 h-5 text-primary" /> <FileText className="w-5 h-5 text-primary" />
@@ -240,17 +389,26 @@ export default function AdminTestsPage() {
)} )}
</div> </div>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<span className="text-sm font-medium">{test.is_active ? "Published" : "Hidden"}</span> <span className="text-sm font-medium">{test.is_active ? "Published" : "Hidden"}</span>
<Switch checked={test.is_active} onCheckedChange={() => handleToggle(test.id)} /> <Switch checked={test.is_active} onCheckedChange={() => handleToggle(test.id)} />
</div> </div>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-destructive hover:bg-destructive/10"
onClick={() => handleDelete(test.id)}
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div> </div>
))} ))}
</div> </div>
)} )}
</CardContent> </CardContent>
</Card> </Card>
</div> </div>
</div> </div>
) )

View File

@@ -5,7 +5,6 @@ import { Button } from "@/components/ui/button"
import { Badge } from "@/components/ui/badge" import { Badge } from "@/components/ui/badge"
import { Clock, Flag, ChevronLeft, ChevronRight, Pause as PauseIcon, MoreVertical, Calculator as CalculatorIcon, FlaskConical, Edit3, XCircle, Play, Shield, AlertTriangle, Moon, Sun } from "lucide-react" import { Clock, Flag, ChevronLeft, ChevronRight, Pause as PauseIcon, MoreVertical, Calculator as CalculatorIcon, FlaskConical, Edit3, XCircle, Play, Shield, AlertTriangle, Moon, Sun } from "lucide-react"
import Image from "next/image" import Image from "next/image"
import { useTheme } from "next-themes"
import { useCBTStore, QuestionStatus } from "@/store/cbtStore" import { useCBTStore, QuestionStatus } from "@/store/cbtStore"
import { useExamTimer } from "@/hooks/useExamTimer" import { useExamTimer } from "@/hooks/useExamTimer"
import ReactMarkdown from "react-markdown" import ReactMarkdown from "react-markdown"
@@ -38,7 +37,29 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
const [showPeriodicTable, setShowPeriodicTable] = useState(false); const [showPeriodicTable, setShowPeriodicTable] = useState(false);
const [isPaused, setIsPaused] = useState(false); const [isPaused, setIsPaused] = useState(false);
const [showSubmitConfirm, setShowSubmitConfirm] = useState(false); const [showSubmitConfirm, setShowSubmitConfirm] = useState(false);
const { theme, setTheme } = useTheme(); const [localTheme, setLocalTheme] = useState<string>('light');
useEffect(() => {
if (typeof window !== 'undefined') {
if (document.documentElement.classList.contains('dark')) {
setLocalTheme('dark');
} else {
setLocalTheme('light');
}
}
}, []);
const handleThemeToggle = () => {
if (document.documentElement.classList.contains('dark')) {
document.documentElement.classList.remove('dark');
localStorage.setItem('theme', 'light');
setLocalTheme('light');
} else {
document.documentElement.classList.add('dark');
localStorage.setItem('theme', 'dark');
setLocalTheme('dark');
}
};
// Anti-cheat state // Anti-cheat state
const [violationCount, setViolationCount] = useState(0); const [violationCount, setViolationCount] = useState(0);
@@ -63,9 +84,6 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
resetExamState, resetExamState,
status, status,
attemptId, attemptId,
currentQuestionId,
getQuestion,
examState
} = useCBTStore(); } = useCBTStore();
const { formattedTime, isTimedOut } = useExamTimer(); const { formattedTime, isTimedOut } = useExamTimer();
@@ -243,8 +261,8 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
<p className="font-semibold">Preferred Theme</p> <p className="font-semibold">Preferred Theme</p>
<p className="text-sm text-muted-foreground">Choose dark or light mode for the exam</p> <p className="text-sm text-muted-foreground">Choose dark or light mode for the exam</p>
</div> </div>
<Button variant="outline" size="icon" onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')}> <Button variant="outline" size="icon" onClick={handleThemeToggle}>
{theme === 'dark' ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />} {localTheme === 'dark' ? <Sun className="h-5 w-5" /> : <Moon className="h-5 w-5" />}
</Button> </Button>
</div> </div>
@@ -267,13 +285,13 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
onRetake={examType === 'DPS' ? () => { onRetake={examType === 'DPS' ? () => {
resetExamState(); resetExamState();
window.location.reload(); window.location.reload();
} : undefined} } : () => {}}
/> />
) )
} }
const currentQuestion = getQuestion(currentQuestionId) const currentQuestion = questions[currentQuestionIndex];
const currentState = currentQuestion ? examState?.answers[currentQuestion.id] : null const currentState = currentQuestion ? answers[currentQuestion.id] : null
const handleOptionSelect = (qId: string, letter: string, type: string) => { const handleOptionSelect = (qId: string, letter: string, type: string) => {
if (type === 'Multiple_Choice') { if (type === 'Multiple_Choice') {
@@ -281,7 +299,11 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
const selectedArray = currentSelected.split(',').filter(Boolean); const selectedArray = currentSelected.split(',').filter(Boolean);
if (selectedArray.includes(letter)) { if (selectedArray.includes(letter)) {
const newArray = selectedArray.filter((l: string) => l !== letter).sort(); const newArray = selectedArray.filter((l: string) => l !== letter).sort();
selectAnswer(qId, newArray.length > 0 ? newArray.join(',') : null); if (newArray.length > 0) {
selectAnswer(qId, newArray.join(','));
} else {
clearResponse(qId);
}
} else { } else {
selectedArray.push(letter); selectedArray.push(letter);
selectAnswer(qId, selectedArray.sort().join(',')); selectAnswer(qId, selectedArray.sort().join(','));
@@ -291,7 +313,8 @@ export default function CBTExamPage({ params }: { params: Promise<{ examId: stri
} }
} }
// Effect to close overlay tools on EscapealetteStats = Object.values(answers).reduce( // Effect to close overlay tools on Escape
const paletteStats = Object.values(answers || {}).reduce(
(acc, val) => { (acc, val) => {
acc[val.status]++; acc[val.status]++;
return acc; return acc;

View File

@@ -118,6 +118,37 @@ export default function AttemptHistoryPage({ params }: { params: Promise<{ attem
const q = questions[ans.question_id]; const q = questions[ans.question_id];
if (!q) return null; if (!q) return null;
const getUserAnswerText = () => {
if (ans.is_subjective || q.type === 'Subjective' || q.type === 'Numerical') {
if (Array.isArray(ans.selected_answer)) {
return ans.selected_answer.join(", ");
}
return String(ans.selected_answer || 'Not Attempted');
}
if (Array.isArray(ans.selected_answer)) {
return ans.selected_answer.map((val: any) => q.options && q.options[val] ? `**${val})** ${q.options[val]}` : val).join("\n\n");
}
return q.options && q.options[ans.selected_answer]
? `**${ans.selected_answer})** ${q.options[ans.selected_answer]}`
: String(ans.selected_answer || 'Not Attempted');
};
const getCorrectAnswerText = () => {
const rawCorrect = q.correct_answer?.correct || q.correct_answer?.value || q.correct_answer?.selected;
if (ans.is_subjective || q.type === 'Subjective' || q.type === 'Numerical') {
if (Array.isArray(rawCorrect)) {
return rawCorrect.join(", ");
}
return String(rawCorrect || 'Subjective text');
}
if (Array.isArray(rawCorrect)) {
return rawCorrect.map((val: any) => q.options && q.options[val] ? `**${val})** ${q.options[val]}` : val).join("\n\n");
}
return q.options && q.options[rawCorrect]
? `**${rawCorrect})** ${q.options[rawCorrect]}`
: String(rawCorrect || 'N/A');
};
return ( return (
<Card key={ans.question_id} className={`border-l-4 ${ans.is_correct ? 'border-l-green-500' : 'border-l-red-500'}`}> <Card key={ans.question_id} className={`border-l-4 ${ans.is_correct ? 'border-l-green-500' : 'border-l-red-500'}`}>
<CardContent className="p-6"> <CardContent className="p-6">
@@ -150,9 +181,7 @@ export default function AttemptHistoryPage({ params }: { params: Promise<{ attem
<p className="text-sm font-bold text-muted-foreground uppercase">Your Answer</p> <p className="text-sm font-bold text-muted-foreground uppercase">Your Answer</p>
<div className={`p-3 rounded-lg border prose prose-sm dark:prose-invert ${ans.is_correct ? 'bg-green-500/10 border-green-500/30 text-green-500' : 'bg-red-500/10 border-red-500/30 text-red-500'}`}> <div className={`p-3 rounded-lg border prose prose-sm dark:prose-invert ${ans.is_correct ? 'bg-green-500/10 border-green-500/30 text-green-500' : 'bg-red-500/10 border-red-500/30 text-red-500'}`}>
<ReactMarkdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex]}> <ReactMarkdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex]}>
{ans.is_subjective || q.type === 'Subjective' || q.type === 'Numerical' {getUserAnswerText()}
? (ans.selected_answer || 'Not Attempted')
: (q.options && q.options[ans.selected_answer] ? `${ans.selected_answer}) ${q.options[ans.selected_answer]}` : ans.selected_answer || 'Not Attempted')}
</ReactMarkdown> </ReactMarkdown>
</div> </div>
</div> </div>
@@ -160,11 +189,7 @@ export default function AttemptHistoryPage({ params }: { params: Promise<{ attem
<p className="text-sm font-bold text-muted-foreground uppercase">Correct Answer</p> <p className="text-sm font-bold text-muted-foreground uppercase">Correct Answer</p>
<div className="p-3 rounded-lg border bg-green-500/10 border-green-500/30 text-green-500 prose prose-sm dark:prose-invert"> <div className="p-3 rounded-lg border bg-green-500/10 border-green-500/30 text-green-500 prose prose-sm dark:prose-invert">
<ReactMarkdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex]}> <ReactMarkdown remarkPlugins={[remarkMath]} rehypePlugins={[rehypeKatex]}>
{ans.is_subjective || q.type === 'Subjective' || q.type === 'Numerical' {getCorrectAnswerText()}
? (q.correct_answer?.value || q.correct_answer?.correct || q.correct_answer?.selected || 'Subjective text')
: (q.options && q.options[q.correct_answer?.correct || q.correct_answer?.value || q.correct_answer?.selected]
? `${q.correct_answer?.correct || q.correct_answer?.value || q.correct_answer?.selected}) ${q.options[q.correct_answer?.correct || q.correct_answer?.value || q.correct_answer?.selected]}`
: (q.correct_answer?.correct || q.correct_answer?.value || q.correct_answer?.selected || 'N/A'))}
</ReactMarkdown> </ReactMarkdown>
</div> </div>
</div> </div>

View File

@@ -15,7 +15,6 @@ export interface UserProfile {
state: string; state: string;
tier: "Free" | "Pro" | "Al_Coach"; // Al_Coach in database schema tier: "Free" | "Pro" | "Al_Coach"; // Al_Coach in database schema
streak_count: number; streak_count: number;
streak_count: number;
longest_streak: number; longest_streak: number;
active_session_token?: string; active_session_token?: string;
created_at?: string; created_at?: string;