Spaces:
Running
Running
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 +13 -6
- nvidia_prompts.py +13 -10
- templates/question_entry_v2.html +112 -32
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.
|
| 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
|
| 126 |
-
|
| 127 |
if data.get('data') and len(data['data']) > 0:
|
| 128 |
-
|
|
|
|
| 129 |
|
| 130 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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. **
|
|
|
|
| 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 |
-
"
|
| 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. **
|
|
|
|
| 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 |
-
"
|
| 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. **
|
|
|
|
| 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 |
-
"
|
| 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. **
|
|
|
|
| 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 |
-
"
|
| 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="
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
//
|
| 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 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
|
|
|
|
|
| 1121 |
|
| 1122 |
btn.disabled = true;
|
| 1123 |
btn.querySelector('.spinner-border').classList.remove('d-none');
|
|
|
|
| 1124 |
|
| 1125 |
try {
|
| 1126 |
-
|
| 1127 |
-
|
| 1128 |
-
|
| 1129 |
-
|
| 1130 |
-
|
| 1131 |
-
|
|
|
|
|
|
|
|
|
|
| 1132 |
})
|
| 1133 |
-
|
| 1134 |
-
|
| 1135 |
-
|
| 1136 |
-
|
| 1137 |
-
|
| 1138 |
-
|
| 1139 |
-
|
| 1140 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1141 |
} else {
|
| 1142 |
-
throw new Error(
|
| 1143 |
}
|
| 1144 |
} catch (e) {
|
| 1145 |
-
|
| 1146 |
-
|
| 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');
|