t commited on
Commit
8fc5307
·
1 Parent(s): a0c684f

feat: add manual classification with AI topic suggestions in question entry v2

Browse files

- Introduced nvidia_prompts.py with specialized NEET syllabus prompts for Biology, Chemistry, Physics, and Mathematics.
- Added /get_topic_suggestions route to fetch AI-powered chapter suggestions using NVIDIA NIM.
- Implemented manual classification wizard in question_entry_v2.html with range selection and topic entry modals.
- Added /classified/update_single route for updating individual question metadata.

classifier_routes.py CHANGED
@@ -8,12 +8,169 @@ from processing import resize_image_if_needed, call_nim_ocr_api
8
  from gemini_classifier import classify_questions_with_gemini
9
  from gemma_classifier import GemmaClassifier
10
  from nova_classifier import classify_questions_with_nova
 
 
11
 
12
  classifier_bp = Blueprint('classifier_bp', __name__)
13
 
14
  # Instantiate classifiers
15
  gemma_classifier = GemmaClassifier()
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  @classifier_bp.route('/classified/edit')
18
  @login_required
19
  def edit_classified_questions():
 
8
  from gemini_classifier import classify_questions_with_gemini
9
  from gemma_classifier import GemmaClassifier
10
  from nova_classifier import classify_questions_with_nova
11
+ import requests
12
+ from nvidia_prompts import BIOLOGY_PROMPT_TEMPLATE, CHEMISTRY_PROMPT_TEMPLATE, PHYSICS_PROMPT_TEMPLATE, MATHEMATICS_PROMPT_TEMPLATE
13
 
14
  classifier_bp = Blueprint('classifier_bp', __name__)
15
 
16
  # Instantiate classifiers
17
  gemma_classifier = GemmaClassifier()
18
 
19
+ def get_nvidia_prompt(subject, input_questions):
20
+ if subject.lower() == 'biology':
21
+ return BIOLOGY_PROMPT_TEMPLATE.format(input_questions=input_questions)
22
+ elif subject.lower() == 'chemistry':
23
+ return CHEMISTRY_PROMPT_TEMPLATE.format(input_questions=input_questions)
24
+ elif subject.lower() == 'physics':
25
+ return PHYSICS_PROMPT_TEMPLATE.format(input_questions=input_questions)
26
+ elif subject.lower() == 'mathematics':
27
+ return MATHEMATICS_PROMPT_TEMPLATE.format(input_questions=input_questions)
28
+ return None
29
+
30
+ @classifier_bp.route('/get_topic_suggestions', methods=['POST'])
31
+ @login_required
32
+ def get_topic_suggestions():
33
+ data = request.json
34
+ question_text = data.get('question_text')
35
+ image_id = data.get('image_id')
36
+ subject = data.get('subject')
37
+
38
+ if not subject:
39
+ return jsonify({'error': 'Subject is required'}), 400
40
+
41
+ # If text is missing but we have image_id, try to get from DB or run OCR
42
+ if not question_text and image_id:
43
+ try:
44
+ conn = get_db_connection()
45
+ # Check DB first
46
+ row = conn.execute('SELECT question_text, processed_filename, session_id FROM questions q JOIN images i ON q.image_id = i.id WHERE i.id = ?', (image_id,)).fetchone()
47
+
48
+ if row:
49
+ if row['question_text']:
50
+ question_text = row['question_text']
51
+ else:
52
+ # Run OCR
53
+ processed_filename = row['processed_filename']
54
+ session_id = row['session_id']
55
+ if processed_filename:
56
+ image_path = os.path.join(current_app.config['PROCESSED_FOLDER'], processed_filename)
57
+ if os.path.exists(image_path):
58
+ image_bytes = resize_image_if_needed(image_path)
59
+ ocr_result = call_nim_ocr_api(image_bytes)
60
+
61
+ if ocr_result.get('data') and ocr_result['data'][0].get('text_detections'):
62
+ question_text = " ".join(item['text_prediction']['text'] for item in ocr_result['data'][0]['text_detections'])
63
+ # Save back to DB
64
+ conn.execute('UPDATE questions SET question_text = ? WHERE image_id = ?', (question_text, image_id))
65
+ conn.commit()
66
+ conn.close()
67
+ except Exception as e:
68
+ current_app.logger.error(f"Error fetching/OCRing text for image {image_id}: {e}")
69
+ return jsonify({'error': f"OCR failed: {str(e)}"}), 500
70
+
71
+ if not question_text:
72
+ return jsonify({'error': 'Could not obtain question text (OCR failed or no text found).'}), 400
73
+
74
+ # Prepare prompt
75
+ # The prompt expects "Input Questions: [Insert ...]".
76
+ # We will format the single question as "1. {text}" to match the pattern somewhat,
77
+ # though the prompt handles raw text too.
78
+ input_formatted = f"1. {question_text}"
79
+ prompt_content = get_nvidia_prompt(subject, input_formatted)
80
+
81
+ if not prompt_content:
82
+ return jsonify({'error': f'Unsupported subject: {subject}'}), 400
83
+
84
+ # Call NVIDIA API
85
+ nvidia_api_key = os.environ.get('NVIDIA_API_KEY')
86
+ if not nvidia_api_key:
87
+ return jsonify({'error': 'NVIDIA_API_KEY not set'}), 500
88
+
89
+ invoke_url = 'https://integrate.api.nvidia.com/v1/chat/completions'
90
+ headers = {
91
+ 'Authorization': f'Bearer {nvidia_api_key}',
92
+ 'Accept': 'application/json',
93
+ 'Content-Type': 'application/json'
94
+ }
95
+
96
+ payload = {
97
+ "model": "nvidia/nemotron-3-nano-30b-a3b",
98
+ "messages": [
99
+ {
100
+ "content": prompt_content,
101
+ "role": "user"
102
+ }
103
+ ],
104
+ "temperature": 0.1, # Low temp for deterministic classification
105
+ "top_p": 1,
106
+ "max_tokens": 1024,
107
+ "stream": False
108
+ }
109
+
110
+ try:
111
+ response = requests.post(invoke_url, headers=headers, json=payload, timeout=30)
112
+ response.raise_for_status()
113
+ result = response.json()
114
+
115
+ content = result['choices'][0]['message']['content']
116
+
117
+ # Parse JSON from content (it might be wrapped in markdown code blocks)
118
+ if "```json" in content:
119
+ content = content.split("```json")[1].split("```")[0].strip()
120
+ elif "```" in content:
121
+ content = content.split("```")[1].split("```")[0].strip()
122
+
123
+ data = json.loads(content)
124
+
125
+ # Extract the chapter title
126
+ chapter_title = "Unclassified"
127
+ if data.get('data') and len(data['data']) > 0:
128
+ chapter_title = data['data'][0].get('chapter_title', 'Unclassified')
129
+
130
+ return jsonify({'success': True, 'chapter_title': chapter_title, 'full_response': data})
131
+
132
+ except Exception as e:
133
+ current_app.logger.error(f"NVIDIA API Error: {e}")
134
+ return jsonify({'error': str(e)}), 500
135
+
136
+ @classifier_bp.route('/classified/update_single', methods=['POST'])
137
+ @login_required
138
+ def update_question_classification_single():
139
+ data = request.json
140
+ image_id = data.get('image_id')
141
+ subject = data.get('subject')
142
+ chapter = data.get('chapter')
143
+
144
+ if not image_id:
145
+ return jsonify({'error': 'Image ID is required'}), 400
146
+
147
+ try:
148
+ conn = get_db_connection()
149
+ # Security: Check ownership via session -> images
150
+ image_owner = conn.execute("""
151
+ SELECT s.user_id
152
+ FROM images i
153
+ JOIN sessions s ON i.session_id = s.id
154
+ WHERE i.id = ?
155
+ """, (image_id,)).fetchone()
156
+
157
+ if not image_owner or image_owner['user_id'] != current_user.id:
158
+ conn.close()
159
+ return jsonify({'error': 'Unauthorized'}), 403
160
+
161
+ conn.execute(
162
+ 'UPDATE questions SET subject = ?, chapter = ? WHERE image_id = ?',
163
+ (subject, chapter, image_id)
164
+ )
165
+ conn.commit()
166
+ conn.close()
167
+ return jsonify({'success': True})
168
+ except Exception as e:
169
+ current_app.logger.error(f"Error updating question classification for image {image_id}: {e}")
170
+ return jsonify({'error': str(e)}), 500
171
+
172
+
173
+
174
  @classifier_bp.route('/classified/edit')
175
  @login_required
176
  def edit_classified_questions():
nvidia_prompts.py ADDED
@@ -0,0 +1,276 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ BIOLOGY_PROMPT_TEMPLATE = """**System Role:** You are a specialized Biology question classifier for NEET exams. Your task is to map questions to their corresponding chapter from the NCERT Biology syllabus.
3
+
4
+ **Syllabus Chapters (Use these exact titles):**
5
+
6
+ **Class XI & XII Combined List**
7
+
8
+ 1. The Living World
9
+ 2. Biological Classification
10
+ 3. Plant Kingdom
11
+ 4. Animal Kingdom
12
+ 5. Morphology of Flowering Plants
13
+ 6. Anatomy of Flowering Plants
14
+ 7. Structural Organisation in Animals
15
+ 8. Cell: The Unit of Life
16
+ 9. Biomolecules
17
+ 10. Cell Cycle and Cell Division
18
+ 11. Photosynthesis in Higher Plants
19
+ 12. Respiration in Plants
20
+ 13. Plant Growth and Development
21
+ 14. Breathing and Exchange of Gases
22
+ 15. Body Fluids and Circulation
23
+ 16. Excretory Products and their Elimination
24
+ 17. Locomotion and Movement
25
+ 18. Neural Control and Coordination
26
+ 19. Chemical Coordination and Integration
27
+ 20. Sexual Reproduction in Flowering Plants
28
+ 21. Human Reproduction
29
+ 22. Reproductive Health
30
+ 23. Principles of Inheritance and Variation
31
+ 24. Molecular Basis of Inheritance
32
+ 25. Evolution
33
+ 26. Health and Disease
34
+ 27. Improvement in Food Production
35
+ 28. Microbes in Human Welfare
36
+ 29. Biotechnology - Principles and Processes
37
+ 30. Biotechnology and Its Applications
38
+ 31. Organisms and Populations
39
+ 32. Ecosystem
40
+ 33. Biodiversity and Its Conservation
41
+
42
+ **Classification Guidelines:**
43
+
44
+ 1. **Scope**: Analyze the input question. If the question is NOT related to Biology, mark the chapter as 'Unclassified'.
45
+ 2. **Mapping**: Identify the single most relevant chapter from the list above.
46
+ 3. **Multi-Chapter**: If a question explicitly spans 2 distinct chapters, include both.
47
+
48
+ **Output JSON Schema:**
49
+
50
+ ```json
51
+ {
52
+ "data": [
53
+ {
54
+ "index": <integer matching question number>,
55
+ "subject": "Biology",
56
+ "chapter_index": <chapter number or 0>,
57
+ "chapter_title": "<exact chapter title from list or 'Unclassified'>",
58
+ "original_question_text": "<complete original question>",
59
+ "confidence": <0.0 to 1.0>
60
+ }
61
+ ],
62
+ "success": [true]
63
+ }
64
+
65
+ ```
66
+
67
+ **Input Questions:**
68
+ {input_questions}
69
+ """
70
+
71
+ CHEMISTRY_PROMPT_TEMPLATE = """**System Role:** You are a specialized Chemistry question classifier for NEET/JEE exams. Your task is to map questions to their corresponding chapter from the NCERT Chemistry
72
+ syllabus.
73
+
74
+ **Syllabus Chapters (Use these exact titles):**
75
+
76
+ **Class XI**
77
+
78
+ 1. Some Basic Concepts of Chemistry
79
+ 2. Structure of Atom
80
+ 3. Classification of Elements and Periodicity in Properties
81
+ 4. Chemical Bonding and Molecular Structure
82
+ 5. States of Matter: Gases and Liquids
83
+ 6. Thermodynamics
84
+ 7. Equilibrium
85
+ 8. Redox Reactions
86
+ 9. Hydrogen
87
+ 10. The s-Block Elements
88
+ 11. The p-Block Elements (Group 13 and 14)
89
+ 12. Organic Chemistry – Some Basic Principles and Techniques (GOC)
90
+ 13. Hydrocarbons
91
+ 14. Environmental Chemistry
92
+
93
+ **Class XII**
94
+
95
+ 1. The Solid State
96
+ 2. Solutions
97
+ 3. Electrochemistry
98
+ 4. Chemical Kinetics
99
+ 5. Surface Chemistry
100
+ 6. General Principles and Processes of Isolation of Elements (Metallurgy)
101
+ 7. The p-Block Elements (Group 15 to 18)
102
+ 8. The d- and f- Block Elements
103
+ 9. Coordination Compounds
104
+ 10. Haloalkanes and Haloarenes
105
+ 11. Alcohols, Phenols and Ethers
106
+ 12. Aldehydes, Ketones and Carboxylic Acids
107
+ 13. Amines
108
+ 14. Biomolecules
109
+ 15. Polymers
110
+ 16. Chemistry in Everyday Life
111
+
112
+ **Classification Guidelines:**
113
+
114
+ 1. **Scope**: Analyze the input question. If the question is NOT related to Chemistry, mark the chapter as 'Unclassified'.
115
+ 2. **Mapping**: Identify the single most relevant chapter from the list above.
116
+ 3. **Multi-Chapter**: If a question explicitly spans 2 distinct chapters, include both.
117
+
118
+ **Output JSON Schema:**
119
+
120
+ ```json
121
+ {
122
+ "data": [
123
+ {
124
+ "index": <integer matching question number>,
125
+ "subject": "Chemistry",
126
+ "chapter_index": <chapter number or 0>,
127
+ "chapter_title": "<exact chapter title from list or 'Unclassified'>",
128
+ "original_question_text": "<complete original question>",
129
+ "confidence": <0.0 to 1.0>
130
+ }
131
+ ],
132
+ "success": [true]
133
+ }
134
+
135
+ ```
136
+
137
+ **Input Questions:**
138
+ {input_questions}
139
+ """
140
+
141
+ PHYSICS_PROMPT_TEMPLATE = """**System Role:** You are a specialized Physics question classifier for NEET/JEE exams. Your task is to map questions to their corresponding chapter from the NCERT Physics syllabus.
142
+
143
+ **Syllabus Chapters (Use these exact titles):**
144
+
145
+ **Class XI**
146
+
147
+ 1. Units and Measurements
148
+ 2. Motion in a Straight Line
149
+ 3. Motion in a Plane
150
+ 4. Laws of Motion
151
+ 5. Work, Energy and Power
152
+ 6. System of Particles and Rotational Motion
153
+ 7. Gravitation
154
+ 8. Mechanical Properties of Solids
155
+ 9. Mechanical Properties of Fluids
156
+ 10. Thermal Properties of Matter
157
+ 11. Thermodynamics
158
+ 12. Kinetic Theory
159
+ 13. Oscillations
160
+ 14. Waves
161
+
162
+ **Class XII**
163
+
164
+ 1. Electric Charges and Fields
165
+ 2. Electrostatic Potential and Capacitance
166
+ 3. Current Electricity
167
+ 4. Moving Charges and Magnetism
168
+ 5. Magnetism and Matter
169
+ 6. Electromagnetic Induction
170
+ 7. Alternating Current
171
+ 8. Electromagnetic Waves
172
+ 9. Ray Optics and Optical Instruments
173
+ 10. Wave Optics
174
+ 11. Dual Nature of Radiation and Matter
175
+ 12. Atoms
176
+ 13. Nuclei
177
+ 14. Semiconductor Electronics: Materials, Devices and Simple Circuits
178
+ 15. Communication Systems
179
+
180
+ **Classification Guidelines:**
181
+
182
+ 1. **Scope**: Analyze the input question. If the question is NOT related to Physics, mark the chapter as 'Unclassified'.
183
+ 2. **Mapping**: Identify the single most relevant chapter from the list above.
184
+ 3. **Multi-Chapter**: If a question explicitly spans 2 distinct chapters, include both.
185
+
186
+ **Output JSON Schema:**
187
+
188
+ ```json
189
+ {
190
+ "data": [
191
+ {
192
+ "index": <integer matching question number>,
193
+ "subject": "Physics",
194
+ "chapter_index": <chapter number or 0>,
195
+ "chapter_title": "<exact chapter title from list or 'Unclassified'>",
196
+ "original_question_text": "<complete original question>",
197
+ "confidence": <0.0 to 1.0>
198
+ }
199
+ ],
200
+ "success": [true]
201
+ }
202
+
203
+ ```
204
+
205
+ **Input Questions:**
206
+ {input_questions}
207
+ """
208
+
209
+ MATHEMATICS_PROMPT_TEMPLATE = """**System Role:** You are a specialized Mathematics question classifier for JEE exams. Your task is to map questions to their corresponding chapter from the NCERT Mathematics
210
+ syllabus.
211
+
212
+ **Syllabus Chapters (Use these exact titles):**
213
+
214
+ **Class XI**
215
+
216
+ 1. Sets
217
+ 2. Relations and Functions
218
+ 3. Trigonometric Functions
219
+ 4. Principle of Mathematical Induction
220
+ 5. Complex Numbers and Quadratic Equations
221
+ 6. Linear Inequalities
222
+ 7. Permutations and Combinations
223
+ 8. Binomial Theorem
224
+ 9. Sequences and Series
225
+ 10. Straight Lines
226
+ 11. Conic Sections
227
+ 12. Introduction to Three Dimensional Geometry
228
+ 13. Limits and Derivatives
229
+ 14. Mathematical Reasoning
230
+ 15. Statistics
231
+ 16. Probability
232
+
233
+ **Class XII**
234
+
235
+ 1. Relations and Functions
236
+ 2. Inverse Trigonometric Functions
237
+ 3. Matrices
238
+ 4. Determinants
239
+ 5. Continuity and Differentiability
240
+ 6. Application of Derivatives
241
+ 7. Integrals
242
+ 8. Application of Integrals
243
+ 9. Differential Equations
244
+ 10. Vector Algebra
245
+ 11. Three Dimensional Geometry
246
+ 12. Linear Programming
247
+ 13. Probability
248
+
249
+ **Classification Guidelines:**
250
+
251
+ 1. **Scope**: Analyze the input question. If the question is NOT related to Mathematics, mark the chapter as 'Unclassified'.
252
+ 2. **Mapping**: Identify the single most relevant chapter from the list above.
253
+ 3. **Multi-Chapter**: If a question explicitly spans 2 distinct chapters, include both.
254
+
255
+ **Output JSON Schema:**
256
+
257
+ ```json
258
+ {
259
+ "data": [
260
+ {
261
+ "index": <integer matching question number>,
262
+ "subject": "Mathematics",
263
+ "chapter_index": <chapter number or 0>,
264
+ "chapter_title": "<exact chapter title from list or 'Unclassified'>",
265
+ "original_question_text": "<complete original question>",
266
+ "confidence": <0.0 to 1.0>
267
+ }
268
+ ],
269
+ "success": [true]
270
+ }
271
+
272
+ ```
273
+
274
+ **Input Questions:**
275
+ {input_questions}
276
+ """
templates/question_entry_v2.html CHANGED
@@ -82,6 +82,9 @@
82
  <span class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
83
  <span class="extract-text">Extract & Classify All</span>
84
  </button>
 
 
 
85
  </div>
86
  {% endif %}
87
 
@@ -270,6 +273,75 @@
270
  </div>
271
  </div>
272
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  {% endblock %}
274
 
275
  {% block scripts %}
@@ -902,6 +974,183 @@
902
  }
903
  }
904
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
905
  document.addEventListener('DOMContentLoaded', () => {
906
  initializeTomSelect(); // Initialize Tom Select here
907
  loadSettings();
 
82
  <span class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
83
  <span class="extract-text">Extract & Classify All</span>
84
  </button>
85
+ <button type="button" class="btn btn-warning ms-2" data-bs-toggle="modal" data-bs-target="#manualClassificationModal">
86
+ <i class="bi bi-list-check"></i> Manual Classification
87
+ </button>
88
  </div>
89
  {% endif %}
90
 
 
273
  </div>
274
  </div>
275
  </div>
276
+
277
+ <!-- Modal 1: Range & Subject -->
278
+ <div class="modal fade" id="manualClassificationModal" tabindex="-1">
279
+ <div class="modal-dialog">
280
+ <div class="modal-content bg-dark text-white">
281
+ <div class="modal-header">
282
+ <h5 class="modal-title">Manual Classification Setup</h5>
283
+ <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
284
+ </div>
285
+ <div class="modal-body">
286
+ <div class="mb-3">
287
+ <label class="form-label">Select Subject</label>
288
+ <select id="manual_subject" class="form-select bg-dark text-white border-secondary">
289
+ <option value="Biology">Biology</option>
290
+ <option value="Chemistry">Chemistry</option>
291
+ <option value="Physics">Physics</option>
292
+ <option value="Mathematics">Mathematics</option>
293
+ </select>
294
+ </div>
295
+ <div class="mb-3">
296
+ <label class="form-label">Question Range (e.g., "1-5, 8, 10-12")</label>
297
+ <input type="text" id="manual_range" class="form-control bg-dark text-white border-secondary" placeholder="Leave empty for all questions">
298
+ <small class="text-muted">Enter ranges like 1-5 or comma separated values.</small>
299
+ </div>
300
+ </div>
301
+ <div class="modal-footer">
302
+ <button type="button" class="btn btn-primary" onclick="startManualClassification()">Start Classification</button>
303
+ </div>
304
+ </div>
305
+ </div>
306
+ </div>
307
+
308
+ <!-- Modal 2: Topic Wizard -->
309
+ <div class="modal fade" id="topicSelectionModal" tabindex="-1" data-bs-backdrop="static">
310
+ <div class="modal-dialog modal-lg">
311
+ <div class="modal-content bg-dark text-white">
312
+ <div class="modal-header">
313
+ <h5 class="modal-title">Classify Question <span id="topic_q_num"></span></h5>
314
+ <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
315
+ </div>
316
+ <div class="modal-body">
317
+ <div class="text-center mb-3">
318
+ <img id="topic_q_img" src="" class="img-fluid rounded" style="max-height: 200px; object-fit: contain;">
319
+ </div>
320
+ <div class="mb-3">
321
+ <label class="form-label">Subject: <span id="topic_subject_display" class="fw-bold text-info"></span></label>
322
+ </div>
323
+ <div class="mb-3">
324
+ <label class="form-label">Chapter / Topic</label>
325
+ <div class="input-group">
326
+ <input type="text" id="topic_input" class="form-control bg-dark text-white border-secondary" placeholder="Enter topic...">
327
+ <button class="btn btn-outline-info" type="button" id="btn_get_suggestion" onclick="getAiSuggestion()">
328
+ <span class="spinner-border spinner-border-sm d-none" role="status"></span>
329
+ ✨ Get AI Suggestion
330
+ </button>
331
+ </div>
332
+ </div>
333
+ <div id="ai_suggestion_result" class="alert alert-info d-none mt-2"></div>
334
+ </div>
335
+ <div class="modal-footer d-flex justify-content-between">
336
+ <button type="button" class="btn btn-secondary" onclick="prevTopicQuestion()">Previous</button>
337
+ <div>
338
+ <span id="topic_progress" class="me-3 text-muted"></span>
339
+ <button type="button" class="btn btn-primary" onclick="nextTopicQuestion()">Next</button>
340
+ </div>
341
+ </div>
342
+ </div>
343
+ </div>
344
+ </div>
345
  {% endblock %}
346
 
347
  {% block scripts %}
 
974
  }
975
  }
976
 
977
+ // --- Manual Classification Logic ---
978
+ let manualQuestionsList = [];
979
+ let currentManualIndex = 0;
980
+ let manualSubject = "";
981
+
982
+ function parseRange(rangeStr, maxVal) {
983
+ const indices = new Set();
984
+ const parts = rangeStr.split(',');
985
+ for (let part of parts) {
986
+ part = part.trim();
987
+ if (!part) continue;
988
+ if (part.includes('-')) {
989
+ const [start, end] = part.split('-').map(Number);
990
+ if (!isNaN(start) && !isNaN(end)) {
991
+ for (let i = start; i <= end; i++) indices.add(i);
992
+ }
993
+ } else {
994
+ const num = Number(part);
995
+ if (!isNaN(num)) indices.add(num);
996
+ }
997
+ }
998
+ // Filter valid 1-based indices and convert to 0-based
999
+ return Array.from(indices).filter(i => i >= 1 && i <= maxVal).map(i => i - 1).sort((a, b) => a - b);
1000
+ }
1001
+
1002
+ function startManualClassification() {
1003
+ manualSubject = document.getElementById('manual_subject').value;
1004
+ const rangeStr = document.getElementById('manual_range').value;
1005
+
1006
+ if (rangeStr.trim() === "") {
1007
+ manualQuestionsList = Array.from({length: numImages}, (_, i) => i);
1008
+ } else {
1009
+ manualQuestionsList = parseRange(rangeStr, numImages);
1010
+ }
1011
+
1012
+ if (manualQuestionsList.length === 0) {
1013
+ alert("No valid questions selected in range.");
1014
+ return;
1015
+ }
1016
+
1017
+ currentManualIndex = 0;
1018
+ const modal1 = bootstrap.Modal.getInstance(document.getElementById('manualClassificationModal'));
1019
+ modal1.hide();
1020
+
1021
+ const modal2 = new bootstrap.Modal(document.getElementById('topicSelectionModal'));
1022
+ modal2.show();
1023
+
1024
+ loadTopicQuestion();
1025
+ }
1026
+
1027
+ function loadTopicQuestion() {
1028
+ const globalIndex = manualQuestionsList[currentManualIndex];
1029
+ // Find the fieldset for this index
1030
+ const fieldset = document.querySelector(`fieldset[data-question-index="${globalIndex}"]`);
1031
+ const imageId = fieldset.querySelector('input[name^="image_id_"]').value;
1032
+ const qNum = fieldset.querySelector('input[name^="question_number_"]').value;
1033
+ const img = fieldset.querySelector('img');
1034
+ const imgUrl = img ? img.src : '';
1035
+
1036
+ // Populate Modal
1037
+ document.getElementById('topic_q_num').innerText = qNum;
1038
+ const modalImg = document.getElementById('topic_q_img');
1039
+ if (imgUrl) {
1040
+ modalImg.src = imgUrl;
1041
+ modalImg.style.display = 'inline-block';
1042
+ } else {
1043
+ modalImg.style.display = 'none';
1044
+ }
1045
+
1046
+ document.getElementById('topic_subject_display').innerText = manualSubject;
1047
+
1048
+ // Get existing chapter if any
1049
+ const chapterSpan = document.getElementById(`chapter_${imageId}`);
1050
+ const currentChapter = chapterSpan ? chapterSpan.innerText : '';
1051
+ document.getElementById('topic_input').value = (currentChapter && currentChapter !== 'N/A' && currentChapter !== 'Unclassified') ? currentChapter : '';
1052
+
1053
+ document.getElementById('topic_progress').innerText = `${currentManualIndex + 1} / ${manualQuestionsList.length}`;
1054
+ const resultDiv = document.getElementById('ai_suggestion_result');
1055
+ resultDiv.classList.add('d-none');
1056
+ resultDiv.innerText = '';
1057
+
1058
+ // Reset button state
1059
+ const btn = document.getElementById('btn_get_suggestion');
1060
+ btn.disabled = false;
1061
+ btn.innerHTML = `<span class="spinner-border spinner-border-sm d-none" role="status"></span> ✨ Get AI Suggestion`;
1062
+ }
1063
+
1064
+ async function nextTopicQuestion() {
1065
+ // Save current
1066
+ await saveCurrentTopic();
1067
+
1068
+ if (currentManualIndex < manualQuestionsList.length - 1) {
1069
+ currentManualIndex++;
1070
+ loadTopicQuestion();
1071
+ } else {
1072
+ // Finish
1073
+ const modal2 = bootstrap.Modal.getInstance(document.getElementById('topicSelectionModal'));
1074
+ modal2.hide();
1075
+ showStatus("Manual classification completed for selected range.", "success");
1076
+ }
1077
+ }
1078
+
1079
+ function prevTopicQuestion() {
1080
+ if (currentManualIndex > 0) {
1081
+ currentManualIndex--;
1082
+ loadTopicQuestion();
1083
+ }
1084
+ }
1085
+
1086
+ async function saveCurrentTopic() {
1087
+ const globalIndex = manualQuestionsList[currentManualIndex];
1088
+ const fieldset = document.querySelector(`fieldset[data-question-index="${globalIndex}"]`);
1089
+ const imageId = fieldset.querySelector('input[name^="image_id_"]').value;
1090
+ const topic = document.getElementById('topic_input').value;
1091
+
1092
+ // Update UI immediately
1093
+ const subjectSpan = document.getElementById(`subject_${imageId}`);
1094
+ if(subjectSpan) subjectSpan.innerText = manualSubject;
1095
+
1096
+ const chapterSpan = document.getElementById(`chapter_${imageId}`);
1097
+ if(chapterSpan) chapterSpan.innerText = topic || 'Unclassified';
1098
+
1099
+ // Send to backend
1100
+ try {
1101
+ await fetch('/classified/update_single', {
1102
+ method: 'POST',
1103
+ headers: { 'Content-Type': 'application/json' },
1104
+ body: JSON.stringify({
1105
+ image_id: imageId,
1106
+ subject: manualSubject,
1107
+ chapter: topic
1108
+ })
1109
+ });
1110
+ } catch (e) {
1111
+ console.error("Failed to save topic", e);
1112
+ }
1113
+ }
1114
+
1115
+ async function getAiSuggestion() {
1116
+ const globalIndex = manualQuestionsList[currentManualIndex];
1117
+ const fieldset = document.querySelector(`fieldset[data-question-index="${globalIndex}"]`);
1118
+ const imageId = fieldset.querySelector('input[name^="image_id_"]').value;
1119
+ const btn = document.getElementById('btn_get_suggestion');
1120
+ const resultDiv = document.getElementById('ai_suggestion_result');
1121
+
1122
+ btn.disabled = true;
1123
+ btn.querySelector('.spinner-border').classList.remove('d-none');
1124
+
1125
+ try {
1126
+ const response = await fetch('/get_topic_suggestions', {
1127
+ method: 'POST',
1128
+ headers: { 'Content-Type': 'application/json' },
1129
+ body: JSON.stringify({
1130
+ image_id: imageId,
1131
+ subject: manualSubject
1132
+ })
1133
+ });
1134
+ const data = await response.json();
1135
+
1136
+ if (data.success) {
1137
+ document.getElementById('topic_input').value = data.chapter_title;
1138
+ resultDiv.innerText = `AI Suggestion: ${data.chapter_title}`;
1139
+ resultDiv.classList.remove('d-none', 'alert-danger');
1140
+ resultDiv.classList.add('alert-info');
1141
+ } else {
1142
+ throw new Error(data.error);
1143
+ }
1144
+ } catch (e) {
1145
+ resultDiv.innerText = `Error: ${e.message}`;
1146
+ resultDiv.classList.remove('d-none', 'alert-info');
1147
+ resultDiv.classList.add('alert-danger');
1148
+ } finally {
1149
+ btn.disabled = false;
1150
+ btn.querySelector('.spinner-border').classList.add('d-none');
1151
+ }
1152
+ }
1153
+
1154
  document.addEventListener('DOMContentLoaded', () => {
1155
  initializeTomSelect(); // Initialize Tom Select here
1156
  loadSettings();