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

fix: resolve ambiguous column error and improve AI suggestions UX

Browse files

- Fix ambiguous 'session_id' in classifier_routes.py SQL query.
- Update nvidia_prompts.py to return multiple chapter suggestions and remove redundant text field.
- Enhance question_entry_v2.html with suggestion pre-fetching, caching, and multi-choice chip UI.

classifier_routes.py CHANGED
@@ -43,7 +43,7 @@ def get_topic_suggestions():
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']:
@@ -101,7 +101,7 @@ def get_topic_suggestions():
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
@@ -122,12 +122,19 @@ def get_topic_suggestions():
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}")
 
43
  try:
44
  conn = get_db_connection()
45
  # Check DB first
46
+ row = conn.execute('SELECT question_text, processed_filename, i.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']:
 
101
  "role": "user"
102
  }
103
  ],
104
+ "temperature": 0.2, # Slightly higher for variety in top-k if supported, but here we just want accurate multiple suggestions
105
  "top_p": 1,
106
  "max_tokens": 1024,
107
  "stream": False
 
122
 
123
  data = json.loads(content)
124
 
125
+ # Extract suggestions
126
+ suggestions = []
127
  if data.get('data') and len(data['data']) > 0:
128
+ primary_chapter = data['data'][0].get('chapter_title', 'Unclassified')
129
+ suggestions.append(primary_chapter)
130
 
131
+ # Check for alternative suggestions if the model provides them (we will update prompts to support this)
132
+ if 'other_possible_chapters' in data['data'][0]:
133
+ others = data['data'][0]['other_possible_chapters']
134
+ if isinstance(others, list):
135
+ suggestions.extend(others)
136
+
137
+ return jsonify({'success': True, 'suggestions': suggestions, 'full_response': data})
138
 
139
  except Exception as e:
140
  current_app.logger.error(f"NVIDIA API Error: {e}")
nvidia_prompts.py CHANGED
@@ -1,4 +1,3 @@
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):**
@@ -43,7 +42,8 @@ BIOLOGY_PROMPT_TEMPLATE = """**System Role:** You are a specialized Biology ques
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
 
@@ -55,7 +55,7 @@ BIOLOGY_PROMPT_TEMPLATE = """**System Role:** You are a specialized Biology ques
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
  ],
@@ -113,7 +113,8 @@ syllabus.
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
 
@@ -125,7 +126,7 @@ syllabus.
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
  ],
@@ -181,7 +182,8 @@ PHYSICS_PROMPT_TEMPLATE = """**System Role:** You are a specialized Physics ques
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
 
@@ -193,7 +195,7 @@ PHYSICS_PROMPT_TEMPLATE = """**System Role:** You are a specialized Physics ques
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
  ],
@@ -250,7 +252,8 @@ syllabus.
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
 
@@ -262,7 +265,7 @@ syllabus.
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
  ],
@@ -273,4 +276,4 @@ syllabus.
273
 
274
  **Input Questions:**
275
  {input_questions}
276
- """
 
 
1
  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.
2
 
3
  **Syllabus Chapters (Use these exact titles):**
 
42
 
43
  1. **Scope**: Analyze the input question. If the question is NOT related to Biology, mark the chapter as 'Unclassified'.
44
  2. **Mapping**: Identify the single most relevant chapter from the list above.
45
+ 3. **Ambiguity**: If other chapters are also plausible, list up to 2 alternatives in 'other_possible_chapters'.
46
+ 4. **Multi-Chapter**: If a question explicitly spans 2 distinct chapters, include the primary one in 'chapter_title' and the secondary in 'other_possible_chapters'.
47
 
48
  **Output JSON Schema:**
49
 
 
55
  "subject": "Biology",
56
  "chapter_index": <chapter number or 0>,
57
  "chapter_title": "<exact chapter title from list or 'Unclassified'>",
58
+ "other_possible_chapters": ["<alternative chapter 1>", "<alternative chapter 2>"],
59
  "confidence": <0.0 to 1.0>
60
  }
61
  ],
 
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. **Ambiguity**: If other chapters are also plausible, list up to 2 alternatives in 'other_possible_chapters'.
117
+ 4. **Multi-Chapter**: If a question explicitly spans 2 distinct chapters, include the primary one in 'chapter_title' and the secondary in 'other_possible_chapters'.
118
 
119
  **Output JSON Schema:**
120
 
 
126
  "subject": "Chemistry",
127
  "chapter_index": <chapter number or 0>,
128
  "chapter_title": "<exact chapter title from list or 'Unclassified'>",
129
+ "other_possible_chapters": ["<alternative chapter 1>", "<alternative chapter 2>"],
130
  "confidence": <0.0 to 1.0>
131
  }
132
  ],
 
182
 
183
  1. **Scope**: Analyze the input question. If the question is NOT related to Physics, mark the chapter as 'Unclassified'.
184
  2. **Mapping**: Identify the single most relevant chapter from the list above.
185
+ 3. **Ambiguity**: If other chapters are also plausible, list up to 2 alternatives in 'other_possible_chapters'.
186
+ 4. **Multi-Chapter**: If a question explicitly spans 2 distinct chapters, include the primary one in 'chapter_title' and the secondary in 'other_possible_chapters'.
187
 
188
  **Output JSON Schema:**
189
 
 
195
  "subject": "Physics",
196
  "chapter_index": <chapter number or 0>,
197
  "chapter_title": "<exact chapter title from list or 'Unclassified'>",
198
+ "other_possible_chapters": ["<alternative chapter 1>", "<alternative chapter 2>"],
199
  "confidence": <0.0 to 1.0>
200
  }
201
  ],
 
252
 
253
  1. **Scope**: Analyze the input question. If the question is NOT related to Mathematics, mark the chapter as 'Unclassified'.
254
  2. **Mapping**: Identify the single most relevant chapter from the list above.
255
+ 3. **Ambiguity**: If other chapters are also plausible, list up to 2 alternatives in 'other_possible_chapters'.
256
+ 4. **Multi-Chapter**: If a question explicitly spans 2 distinct chapters, include the primary one in 'chapter_title' and the secondary in 'other_possible_chapters'.
257
 
258
  **Output JSON Schema:**
259
 
 
265
  "subject": "Mathematics",
266
  "chapter_index": <chapter number or 0>,
267
  "chapter_title": "<exact chapter title from list or 'Unclassified'>",
268
+ "other_possible_chapters": ["<alternative chapter 1>", "<alternative chapter 2>"],
269
  "confidence": <0.0 to 1.0>
270
  }
271
  ],
 
276
 
277
  **Input Questions:**
278
  {input_questions}
279
+ """
templates/question_entry_v2.html CHANGED
@@ -330,7 +330,11 @@
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>
@@ -978,6 +982,7 @@
978
  let manualQuestionsList = [];
979
  let currentManualIndex = 0;
980
  let manualSubject = "";
 
981
 
982
  function parseRange(rangeStr, maxVal) {
983
  const indices = new Set();
@@ -995,7 +1000,6 @@
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
 
@@ -1015,6 +1019,8 @@
1015
  }
1016
 
1017
  currentManualIndex = 0;
 
 
1018
  const modal1 = bootstrap.Modal.getInstance(document.getElementById('manualClassificationModal'));
1019
  modal1.hide();
1020
 
@@ -1022,18 +1028,48 @@
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) {
@@ -1045,31 +1081,49 @@
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");
@@ -1089,14 +1143,12 @@
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',
@@ -1117,34 +1169,62 @@
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');
 
330
  </button>
331
  </div>
332
  </div>
333
+ <div id="ai_suggestion_container" class="d-none mt-2">
334
+ <label class="form-label text-muted small">AI Suggestions:</label>
335
+ <div id="ai_suggestion_chips" class="d-flex flex-wrap gap-2"></div>
336
+ </div>
337
+ <div id="ai_error_msg" class="alert alert-danger d-none mt-2"></div>
338
  </div>
339
  <div class="modal-footer d-flex justify-content-between">
340
  <button type="button" class="btn btn-secondary" onclick="prevTopicQuestion()">Previous</button>
 
982
  let manualQuestionsList = [];
983
  let currentManualIndex = 0;
984
  let manualSubject = "";
985
+ let suggestionCache = new Map(); // Stores promises or results: imageId -> {suggestions: [], status: 'pending'|'done'}
986
 
987
  function parseRange(rangeStr, maxVal) {
988
  const indices = new Set();
 
1000
  if (!isNaN(num)) indices.add(num);
1001
  }
1002
  }
 
1003
  return Array.from(indices).filter(i => i >= 1 && i <= maxVal).map(i => i - 1).sort((a, b) => a - b);
1004
  }
1005
 
 
1019
  }
1020
 
1021
  currentManualIndex = 0;
1022
+ suggestionCache.clear(); // Clear cache on new run
1023
+
1024
  const modal1 = bootstrap.Modal.getInstance(document.getElementById('manualClassificationModal'));
1025
  modal1.hide();
1026
 
 
1028
  modal2.show();
1029
 
1030
  loadTopicQuestion();
1031
+
1032
+ // Trigger prefetch for the first few questions
1033
+ prefetchSuggestions(0, 2);
1034
+ }
1035
+
1036
+ async function prefetchSuggestions(startIndex, count) {
1037
+ for (let i = startIndex; i < startIndex + count && i < manualQuestionsList.length; i++) {
1038
+ const globalIndex = manualQuestionsList[i];
1039
+ const fieldset = document.querySelector(`fieldset[data-question-index="${globalIndex}"]`);
1040
+ const imageId = fieldset.querySelector('input[name^="image_id_"]').value;
1041
+
1042
+ if (!suggestionCache.has(imageId)) {
1043
+ // Initiate fetch and store the promise
1044
+ const promise = fetch('/get_topic_suggestions', {
1045
+ method: 'POST',
1046
+ headers: { 'Content-Type': 'application/json' },
1047
+ body: JSON.stringify({ image_id: imageId, subject: manualSubject })
1048
+ })
1049
+ .then(res => res.json())
1050
+ .then(data => {
1051
+ if (data.success) {
1052
+ return { status: 'done', suggestions: data.suggestions || [data.chapter_title] };
1053
+ } else {
1054
+ throw new Error(data.error);
1055
+ }
1056
+ })
1057
+ .catch(err => ({ status: 'error', error: err.message }));
1058
+
1059
+ suggestionCache.set(imageId, promise);
1060
+ }
1061
+ }
1062
  }
1063
 
1064
  function loadTopicQuestion() {
1065
  const globalIndex = manualQuestionsList[currentManualIndex];
 
1066
  const fieldset = document.querySelector(`fieldset[data-question-index="${globalIndex}"]`);
1067
  const imageId = fieldset.querySelector('input[name^="image_id_"]').value;
1068
  const qNum = fieldset.querySelector('input[name^="question_number_"]').value;
1069
  const img = fieldset.querySelector('img');
1070
  const imgUrl = img ? img.src : '';
1071
 
1072
+ // UI Updates
1073
  document.getElementById('topic_q_num').innerText = qNum;
1074
  const modalImg = document.getElementById('topic_q_img');
1075
  if (imgUrl) {
 
1081
 
1082
  document.getElementById('topic_subject_display').innerText = manualSubject;
1083
 
 
1084
  const chapterSpan = document.getElementById(`chapter_${imageId}`);
1085
  const currentChapter = chapterSpan ? chapterSpan.innerText : '';
1086
  document.getElementById('topic_input').value = (currentChapter && currentChapter !== 'N/A' && currentChapter !== 'Unclassified') ? currentChapter : '';
1087
 
1088
  document.getElementById('topic_progress').innerText = `${currentManualIndex + 1} / ${manualQuestionsList.length}`;
 
 
 
1089
 
1090
+ // Clear previous suggestions
1091
+ document.getElementById('ai_suggestion_container').classList.add('d-none');
1092
+ document.getElementById('ai_suggestion_chips').innerHTML = '';
1093
+ document.getElementById('ai_error_msg').classList.add('d-none');
1094
+
1095
+ // Reset button
1096
  const btn = document.getElementById('btn_get_suggestion');
1097
  btn.disabled = false;
1098
  btn.innerHTML = `<span class="spinner-border spinner-border-sm d-none" role="status"></span> ✨ Get AI Suggestion`;
1099
+
1100
+ // Check cache for instant load (optional: auto-show if cached?)
1101
+ // Let's stick to user clicking for now, or we can auto-load.
1102
+ // The user asked "auto get multiple ai suggestions". So we should try to show them if available.
1103
+ if (suggestionCache.has(imageId)) {
1104
+ // If we have it (or it's loading), we can try to show it automatically or just wait for click.
1105
+ // Given "auto get", let's simulate a click if the user hasn't entered a topic yet.
1106
+ if (!document.getElementById('topic_input').value) {
1107
+ getAiSuggestion();
1108
+ }
1109
+ } else {
1110
+ // Not in cache, start fetching now (prefetch logic handles this, but ensuring current is fetched)
1111
+ prefetchSuggestions(currentManualIndex, 1);
1112
+ if (!document.getElementById('topic_input').value) {
1113
+ getAiSuggestion();
1114
+ }
1115
+ }
1116
  }
1117
 
1118
  async function nextTopicQuestion() {
 
1119
  await saveCurrentTopic();
1120
 
1121
  if (currentManualIndex < manualQuestionsList.length - 1) {
1122
  currentManualIndex++;
1123
  loadTopicQuestion();
1124
+ // Prefetch next few
1125
+ prefetchSuggestions(currentManualIndex + 1, 2);
1126
  } else {
 
1127
  const modal2 = bootstrap.Modal.getInstance(document.getElementById('topicSelectionModal'));
1128
  modal2.hide();
1129
  showStatus("Manual classification completed for selected range.", "success");
 
1143
  const imageId = fieldset.querySelector('input[name^="image_id_"]').value;
1144
  const topic = document.getElementById('topic_input').value;
1145
 
 
1146
  const subjectSpan = document.getElementById(`subject_${imageId}`);
1147
  if(subjectSpan) subjectSpan.innerText = manualSubject;
1148
 
1149
  const chapterSpan = document.getElementById(`chapter_${imageId}`);
1150
  if(chapterSpan) chapterSpan.innerText = topic || 'Unclassified';
1151
 
 
1152
  try {
1153
  await fetch('/classified/update_single', {
1154
  method: 'POST',
 
1169
  const fieldset = document.querySelector(`fieldset[data-question-index="${globalIndex}"]`);
1170
  const imageId = fieldset.querySelector('input[name^="image_id_"]').value;
1171
  const btn = document.getElementById('btn_get_suggestion');
1172
+ const container = document.getElementById('ai_suggestion_container');
1173
+ const chipsContainer = document.getElementById('ai_suggestion_chips');
1174
+ const errorDiv = document.getElementById('ai_error_msg');
1175
 
1176
  btn.disabled = true;
1177
  btn.querySelector('.spinner-border').classList.remove('d-none');
1178
+ errorDiv.classList.add('d-none');
1179
 
1180
  try {
1181
+ let result;
1182
+ if (suggestionCache.has(imageId)) {
1183
+ result = await suggestionCache.get(imageId);
1184
+ } else {
1185
+ // Should be covered by prefetch, but fallback just in case
1186
+ const promise = fetch('/get_topic_suggestions', {
1187
+ method: 'POST',
1188
+ headers: { 'Content-Type': 'application/json' },
1189
+ body: JSON.stringify({ image_id: imageId, subject: manualSubject })
1190
  })
1191
+ .then(res => res.json())
1192
+ .then(data => {
1193
+ if (data.success) return { status: 'done', suggestions: data.suggestions || [data.chapter_title] };
1194
+ else throw new Error(data.error);
1195
+ })
1196
+ .catch(err => ({ status: 'error', error: err.message }));
1197
+
1198
+ suggestionCache.set(imageId, promise);
1199
+ result = await promise;
1200
+ }
1201
+
1202
+ if (result.status === 'done') {
1203
+ container.classList.remove('d-none');
1204
+ chipsContainer.innerHTML = '';
1205
+
1206
+ result.suggestions.forEach(suggestion => {
1207
+ const chip = document.createElement('button');
1208
+ chip.className = 'btn btn-outline-info btn-sm rounded-pill';
1209
+ chip.innerText = suggestion;
1210
+ chip.onclick = () => {
1211
+ document.getElementById('topic_input').value = suggestion;
1212
+ };
1213
+ chipsContainer.appendChild(chip);
1214
+ });
1215
+
1216
+ // Auto-fill input if empty and we have a primary suggestion
1217
+ const currentVal = document.getElementById('topic_input').value;
1218
+ if (!currentVal && result.suggestions.length > 0) {
1219
+ document.getElementById('topic_input').value = result.suggestions[0];
1220
+ }
1221
+
1222
  } else {
1223
+ throw new Error(result.error);
1224
  }
1225
  } catch (e) {
1226
+ errorDiv.innerText = `Error: ${e.message}`;
1227
+ errorDiv.classList.remove('d-none');
 
1228
  } finally {
1229
  btn.disabled = false;
1230
  btn.querySelector('.spinner-border').classList.add('d-none');