Spaces:
Running
Running
root commited on
Commit ·
77cff06
1
Parent(s): 7d4265e
feat: enhance session collections, automate classification sync, and overhaul manual classification wizard UI/UX
Browse files- Automated sync of classified questions to NEETprep collections via sync_neetprep_collection helper.
- Standard sessions with classified questions can now be viewed directly as collections without duplication.
- Overhauled manual classification wizard in question_entry_v2 with Shift+1-4/~ subject shortcuts and Arrow cycling for AI suggestions.
- Improved 'Double Enter' flow for rapid classification.
- Added Zoology support to classification systems.
- Unified dashboard view with 'View as Collection' for applicable sessions.
- Fixed CSS leak in question_entry_v2.html.
- Fixed BuildError for collection view links.
- classifier_routes.py +18 -1
- dashboard.py +29 -50
- neetprep.py +112 -23
- routes/questions.py +19 -0
- templates/_revision_notes.html +205 -573
- templates/dashboard.html +12 -41
- templates/question_entry_v2.html +154 -30
- templates/templates/question_entry_v2.html +1 -8
- utils.py +31 -0
classifier_routes.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
from flask import Blueprint, jsonify, current_app, render_template, request
|
| 2 |
from flask_login import login_required, current_user
|
| 3 |
-
from utils import get_db_connection
|
| 4 |
import os
|
| 5 |
import time
|
| 6 |
import json
|
|
@@ -169,6 +169,12 @@ def update_question_classification_single():
|
|
| 169 |
'UPDATE questions SET subject = ?, chapter = ? WHERE image_id = ?',
|
| 170 |
(subject, chapter, image_id)
|
| 171 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
conn.commit()
|
| 173 |
conn.close()
|
| 174 |
return jsonify({'success': True})
|
|
@@ -262,6 +268,9 @@ def delete_classified_question(question_id):
|
|
| 262 |
# Update the question to remove classification
|
| 263 |
conn.execute('UPDATE questions SET subject = NULL, chapter = NULL WHERE id = ?', (question_id,))
|
| 264 |
|
|
|
|
|
|
|
|
|
|
| 265 |
conn.commit()
|
| 266 |
conn.close()
|
| 267 |
return jsonify({'success': True})
|
|
@@ -298,6 +307,10 @@ def delete_many_classified_questions():
|
|
| 298 |
update_placeholders = ','.join('?' for _ in owned_q_ids)
|
| 299 |
conn.execute(f'UPDATE questions SET subject = NULL, chapter = NULL WHERE id IN ({update_placeholders})', owned_q_ids)
|
| 300 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
conn.commit()
|
| 302 |
conn.close()
|
| 303 |
return jsonify({'success': True})
|
|
@@ -452,6 +465,10 @@ def extract_and_classify_all(session_id):
|
|
| 452 |
current_app.logger.info("Waiting 5 seconds before next batch...")
|
| 453 |
time.sleep(5)
|
| 454 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 455 |
conn.close()
|
| 456 |
|
| 457 |
return jsonify({'success': True, 'message': f'Successfully extracted and classified {total_questions} questions. Updated {total_update_count} entries in the database.'})
|
|
|
|
| 1 |
from flask import Blueprint, jsonify, current_app, render_template, request
|
| 2 |
from flask_login import login_required, current_user
|
| 3 |
+
from utils import get_db_connection, sync_neetprep_collection
|
| 4 |
import os
|
| 5 |
import time
|
| 6 |
import json
|
|
|
|
| 169 |
'UPDATE questions SET subject = ?, chapter = ? WHERE image_id = ?',
|
| 170 |
(subject, chapter, image_id)
|
| 171 |
)
|
| 172 |
+
|
| 173 |
+
# Auto-convert and sync neetprep collection
|
| 174 |
+
img_row = conn.execute('SELECT session_id FROM images WHERE id = ?', (image_id,)).fetchone()
|
| 175 |
+
if img_row:
|
| 176 |
+
sync_neetprep_collection(conn, img_row['session_id'], current_user.id)
|
| 177 |
+
|
| 178 |
conn.commit()
|
| 179 |
conn.close()
|
| 180 |
return jsonify({'success': True})
|
|
|
|
| 268 |
# Update the question to remove classification
|
| 269 |
conn.execute('UPDATE questions SET subject = NULL, chapter = NULL WHERE id = ?', (question_id,))
|
| 270 |
|
| 271 |
+
# Remove bookmark too
|
| 272 |
+
conn.execute('DELETE FROM neetprep_bookmarks WHERE neetprep_question_id = ? AND question_type = ?', (str(question_id), 'classified'))
|
| 273 |
+
|
| 274 |
conn.commit()
|
| 275 |
conn.close()
|
| 276 |
return jsonify({'success': True})
|
|
|
|
| 307 |
update_placeholders = ','.join('?' for _ in owned_q_ids)
|
| 308 |
conn.execute(f'UPDATE questions SET subject = NULL, chapter = NULL WHERE id IN ({update_placeholders})', owned_q_ids)
|
| 309 |
|
| 310 |
+
# Remove bookmarks too
|
| 311 |
+
owned_ids_str = [str(qid) for qid in owned_q_ids]
|
| 312 |
+
conn.execute(f"DELETE FROM neetprep_bookmarks WHERE neetprep_question_id IN ({update_placeholders}) AND question_type = 'classified'", owned_ids_str)
|
| 313 |
+
|
| 314 |
conn.commit()
|
| 315 |
conn.close()
|
| 316 |
return jsonify({'success': True})
|
|
|
|
| 465 |
current_app.logger.info("Waiting 5 seconds before next batch...")
|
| 466 |
time.sleep(5)
|
| 467 |
|
| 468 |
+
# Auto-convert and sync neetprep collection
|
| 469 |
+
sync_neetprep_collection(conn, session_id, current_user.id)
|
| 470 |
+
|
| 471 |
+
conn.commit()
|
| 472 |
conn.close()
|
| 473 |
|
| 474 |
return jsonify({'success': True, 'message': f'Successfully extracted and classified {total_questions} questions. Updated {total_update_count} entries in the database.'})
|
dashboard.py
CHANGED
|
@@ -137,58 +137,37 @@ def dashboard():
|
|
| 137 |
|
| 138 |
conn = get_db_connection()
|
| 139 |
|
| 140 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
if filter_type == 'collections':
|
| 142 |
-
|
| 143 |
-
sessions_rows = conn.execute("""
|
| 144 |
-
SELECT s.id, s.created_at, s.original_filename, s.persist, s.name, s.session_type, s.group_name,
|
| 145 |
-
0 as page_count,
|
| 146 |
-
COUNT(nb.id) as question_count
|
| 147 |
-
FROM sessions s
|
| 148 |
-
LEFT JOIN neetprep_bookmarks nb ON s.id = nb.session_id
|
| 149 |
-
WHERE s.user_id = ? AND s.session_type = 'neetprep_collection'
|
| 150 |
-
GROUP BY s.id, s.created_at, s.original_filename, s.persist, s.name, s.session_type, s.group_name
|
| 151 |
-
ORDER BY s.created_at DESC
|
| 152 |
-
""", (current_user.id,)).fetchall()
|
| 153 |
elif filter_type == 'standard':
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
GROUP BY s.id, s.created_at, s.original_filename, s.persist, s.name, s.session_type, s.group_name
|
| 163 |
-
ORDER BY s.created_at DESC
|
| 164 |
-
""", (current_user.id,)).fetchall()
|
| 165 |
-
else:
|
| 166 |
-
# Show all (both standard and collections, but not final_pdf)
|
| 167 |
-
# First get standard sessions
|
| 168 |
-
standard_sessions = conn.execute("""
|
| 169 |
-
SELECT s.id, s.created_at, s.original_filename, s.persist, s.name, s.session_type, s.group_name,
|
| 170 |
-
COUNT(CASE WHEN i.image_type = 'original' THEN 1 END) as page_count,
|
| 171 |
-
COUNT(CASE WHEN i.image_type = 'cropped' THEN 1 END) as question_count
|
| 172 |
-
FROM sessions s
|
| 173 |
-
LEFT JOIN images i ON s.id = i.session_id
|
| 174 |
-
WHERE s.user_id = ? AND (s.session_type IS NULL OR s.session_type NOT IN ('final_pdf', 'neetprep_collection'))
|
| 175 |
-
GROUP BY s.id, s.created_at, s.original_filename, s.persist, s.name, s.session_type, s.group_name
|
| 176 |
-
""", (current_user.id,)).fetchall()
|
| 177 |
-
|
| 178 |
-
# Then get neetprep collections
|
| 179 |
-
collection_sessions = conn.execute("""
|
| 180 |
-
SELECT s.id, s.created_at, s.original_filename, s.persist, s.name, s.session_type, s.group_name,
|
| 181 |
-
0 as page_count,
|
| 182 |
-
COUNT(nb.id) as question_count
|
| 183 |
-
FROM sessions s
|
| 184 |
-
LEFT JOIN neetprep_bookmarks nb ON s.id = nb.session_id
|
| 185 |
-
WHERE s.user_id = ? AND s.session_type = 'neetprep_collection'
|
| 186 |
-
GROUP BY s.id, s.created_at, s.original_filename, s.persist, s.name, s.session_type, s.group_name
|
| 187 |
-
""", (current_user.id,)).fetchall()
|
| 188 |
-
|
| 189 |
-
# Combine and sort by created_at
|
| 190 |
-
all_sessions = list(standard_sessions) + list(collection_sessions)
|
| 191 |
-
sessions_rows = sorted(all_sessions, key=lambda x: x['created_at'], reverse=True)
|
| 192 |
|
| 193 |
sessions = []
|
| 194 |
for session in sessions_rows:
|
|
|
|
| 137 |
|
| 138 |
conn = get_db_connection()
|
| 139 |
|
| 140 |
+
# Unified query for all session types with classified check
|
| 141 |
+
query = """
|
| 142 |
+
SELECT s.id, s.created_at, s.original_filename, s.persist, s.name, s.session_type, s.group_name,
|
| 143 |
+
COUNT(DISTINCT CASE WHEN i.image_type = 'original' THEN i.id END) as page_count,
|
| 144 |
+
(COUNT(DISTINCT CASE WHEN i.image_type = 'cropped' THEN i.id END) +
|
| 145 |
+
COUNT(DISTINCT nb.id)) as question_count,
|
| 146 |
+
EXISTS(
|
| 147 |
+
SELECT 1 FROM questions q
|
| 148 |
+
WHERE q.session_id = s.id
|
| 149 |
+
AND q.subject IS NOT NULL AND q.subject != 'Unclassified'
|
| 150 |
+
AND q.chapter IS NOT NULL AND q.chapter != 'Unclassified'
|
| 151 |
+
) as has_classified
|
| 152 |
+
FROM sessions s
|
| 153 |
+
LEFT JOIN images i ON s.id = i.session_id
|
| 154 |
+
LEFT JOIN neetprep_bookmarks nb ON s.id = nb.session_id
|
| 155 |
+
WHERE s.user_id = ? AND (s.session_type IS NULL OR s.session_type != 'final_pdf')
|
| 156 |
+
"""
|
| 157 |
+
|
| 158 |
+
params = [current_user.id]
|
| 159 |
+
|
| 160 |
if filter_type == 'collections':
|
| 161 |
+
query += " AND s.session_type = 'neetprep_collection'"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
elif filter_type == 'standard':
|
| 163 |
+
query += " AND (s.session_type IS NULL OR s.session_type = 'standard')"
|
| 164 |
+
|
| 165 |
+
query += """
|
| 166 |
+
GROUP BY s.id, s.created_at, s.original_filename, s.persist, s.name, s.session_type, s.group_name
|
| 167 |
+
ORDER BY s.created_at DESC
|
| 168 |
+
"""
|
| 169 |
+
|
| 170 |
+
sessions_rows = conn.execute(query, params).fetchall()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 171 |
|
| 172 |
sessions = []
|
| 173 |
for session in sessions_rows:
|
neetprep.py
CHANGED
|
@@ -1073,7 +1073,7 @@ def view_collection(session_id):
|
|
| 1073 |
flash('Collection not found', 'danger')
|
| 1074 |
return redirect(url_for('dashboard.dashboard', filter='collections'))
|
| 1075 |
|
| 1076 |
-
# Fetch neetprep questions
|
| 1077 |
neetprep_questions = conn.execute("""
|
| 1078 |
SELECT nq.id, nq.question_text, nq.options, nq.correct_answer_index,
|
| 1079 |
nq.level, nq.topic, nq.subject, b.created_at as bookmarked_at, 'neetprep' as question_type
|
|
@@ -1083,8 +1083,8 @@ def view_collection(session_id):
|
|
| 1083 |
ORDER BY nq.topic, b.created_at
|
| 1084 |
""", (session_id, current_user.id)).fetchall()
|
| 1085 |
|
| 1086 |
-
# Fetch classified questions
|
| 1087 |
-
|
| 1088 |
SELECT q.id, q.question_text, NULL as options, q.actual_solution as correct_answer_index,
|
| 1089 |
NULL as level, q.chapter as topic, q.subject, b.created_at as bookmarked_at, 'classified' as question_type,
|
| 1090 |
i.processed_filename as image_filename, q.question_number
|
|
@@ -1095,16 +1095,44 @@ def view_collection(session_id):
|
|
| 1095 |
ORDER BY q.chapter, b.created_at
|
| 1096 |
""", (session_id, current_user.id)).fetchall()
|
| 1097 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1098 |
conn.close()
|
| 1099 |
|
| 1100 |
-
# Combine all questions
|
| 1101 |
all_questions = []
|
|
|
|
|
|
|
|
|
|
| 1102 |
for q in neetprep_questions:
|
| 1103 |
qd = dict(q)
|
| 1104 |
qd['image_filename'] = None
|
| 1105 |
all_questions.append(qd)
|
| 1106 |
-
|
| 1107 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1108 |
|
| 1109 |
# Group questions by topic
|
| 1110 |
topics = {}
|
|
@@ -1203,7 +1231,38 @@ def collection_quiz(session_id):
|
|
| 1203 |
ORDER BY b.created_at
|
| 1204 |
""", (session_id, current_user.id)).fetchall()
|
| 1205 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1206 |
for q in neetprep_questions:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1207 |
try:
|
| 1208 |
html_content = f"""<html><head><meta charset="utf-8"></head><body>{q['question_text']}</body></html>"""
|
| 1209 |
img_filename = f"neetprep_{q['id']}.jpg"
|
|
@@ -1224,18 +1283,12 @@ def collection_quiz(session_id):
|
|
| 1224 |
except Exception as e:
|
| 1225 |
current_app.logger.error(f"Failed to convert question {q['id']} to image: {e}")
|
| 1226 |
|
| 1227 |
-
#
|
| 1228 |
-
|
| 1229 |
-
|
| 1230 |
-
|
| 1231 |
-
|
| 1232 |
-
|
| 1233 |
-
LEFT JOIN images i ON q.image_id = i.id
|
| 1234 |
-
WHERE b.session_id = ? AND b.user_id = ? AND b.question_type = 'classified'
|
| 1235 |
-
ORDER BY b.created_at
|
| 1236 |
-
""", (session_id, current_user.id)).fetchall()
|
| 1237 |
-
|
| 1238 |
-
for q in classified_questions:
|
| 1239 |
if q['processed_filename']:
|
| 1240 |
all_questions.append({
|
| 1241 |
'image_path': f"/processed/{q['processed_filename']}",
|
|
@@ -1251,7 +1304,13 @@ def collection_quiz(session_id):
|
|
| 1251 |
}
|
| 1252 |
})
|
| 1253 |
|
| 1254 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1255 |
|
| 1256 |
if not all_questions:
|
| 1257 |
from flask import redirect, flash
|
|
@@ -1283,8 +1342,8 @@ def generate_collection_pdf(session_id):
|
|
| 1283 |
ORDER BY nq.topic, b.created_at
|
| 1284 |
""", (session_id, current_user.id)).fetchall()
|
| 1285 |
|
| 1286 |
-
# Get
|
| 1287 |
-
|
| 1288 |
SELECT q.id, q.question_text, q.actual_solution as correct_answer_index,
|
| 1289 |
q.chapter as topic, q.subject, i.processed_filename
|
| 1290 |
FROM neetprep_bookmarks b
|
|
@@ -1294,15 +1353,32 @@ def generate_collection_pdf(session_id):
|
|
| 1294 |
ORDER BY q.chapter, b.created_at
|
| 1295 |
""", (session_id, current_user.id)).fetchall()
|
| 1296 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1297 |
conn.close()
|
| 1298 |
|
| 1299 |
-
if not neetprep_questions and not
|
| 1300 |
return jsonify({'error': 'No questions in this collection'}), 400
|
| 1301 |
|
| 1302 |
data = request.json or {}
|
| 1303 |
all_questions = []
|
|
|
|
| 1304 |
|
|
|
|
| 1305 |
for q in neetprep_questions:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1306 |
all_questions.append({
|
| 1307 |
"id": q['id'],
|
| 1308 |
"question_text": q['question_text'],
|
|
@@ -1318,7 +1394,12 @@ def generate_collection_pdf(session_id):
|
|
| 1318 |
}
|
| 1319 |
})
|
| 1320 |
|
| 1321 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1322 |
if q['processed_filename']:
|
| 1323 |
# Use absolute path for PDF generation
|
| 1324 |
abs_img_path = os.path.abspath(os.path.join(current_app.config['PROCESSED_FOLDER'], q['processed_filename']))
|
|
@@ -1336,6 +1417,14 @@ def generate_collection_pdf(session_id):
|
|
| 1336 |
}
|
| 1337 |
})
|
| 1338 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1339 |
topics = list(set(q['custom_fields']['topic'] for q in all_questions if q['custom_fields'].get('topic')))
|
| 1340 |
|
| 1341 |
final_json_output = {
|
|
|
|
| 1073 |
flash('Collection not found', 'danger')
|
| 1074 |
return redirect(url_for('dashboard.dashboard', filter='collections'))
|
| 1075 |
|
| 1076 |
+
# Fetch neetprep questions from bookmarks
|
| 1077 |
neetprep_questions = conn.execute("""
|
| 1078 |
SELECT nq.id, nq.question_text, nq.options, nq.correct_answer_index,
|
| 1079 |
nq.level, nq.topic, nq.subject, b.created_at as bookmarked_at, 'neetprep' as question_type
|
|
|
|
| 1083 |
ORDER BY nq.topic, b.created_at
|
| 1084 |
""", (session_id, current_user.id)).fetchall()
|
| 1085 |
|
| 1086 |
+
# Fetch explicitly bookmarked classified questions
|
| 1087 |
+
bookmarked_classified = conn.execute("""
|
| 1088 |
SELECT q.id, q.question_text, NULL as options, q.actual_solution as correct_answer_index,
|
| 1089 |
NULL as level, q.chapter as topic, q.subject, b.created_at as bookmarked_at, 'classified' as question_type,
|
| 1090 |
i.processed_filename as image_filename, q.question_number
|
|
|
|
| 1095 |
ORDER BY q.chapter, b.created_at
|
| 1096 |
""", (session_id, current_user.id)).fetchall()
|
| 1097 |
|
| 1098 |
+
# Fetch ALL classified questions from the session (even if not bookmarked)
|
| 1099 |
+
# This enables "View as Collection" for any standard session
|
| 1100 |
+
session_classified = conn.execute("""
|
| 1101 |
+
SELECT q.id, q.question_text, NULL as options, q.actual_solution as correct_answer_index,
|
| 1102 |
+
NULL as level, q.chapter as topic, q.subject, q.id as bookmarked_at, 'classified' as question_type,
|
| 1103 |
+
i.processed_filename as image_filename, q.question_number
|
| 1104 |
+
FROM questions q
|
| 1105 |
+
LEFT JOIN images i ON q.image_id = i.id
|
| 1106 |
+
WHERE q.session_id = ? AND q.subject IS NOT NULL AND q.subject != 'Unclassified'
|
| 1107 |
+
AND q.chapter IS NOT NULL AND q.chapter != 'Unclassified'
|
| 1108 |
+
ORDER BY q.chapter, q.id
|
| 1109 |
+
""", (session_id,)).fetchall()
|
| 1110 |
+
|
| 1111 |
conn.close()
|
| 1112 |
|
| 1113 |
+
# Combine all questions, ensuring uniqueness by question ID
|
| 1114 |
all_questions = []
|
| 1115 |
+
seen_ids = set()
|
| 1116 |
+
|
| 1117 |
+
# Add bookmarked neetprep questions
|
| 1118 |
for q in neetprep_questions:
|
| 1119 |
qd = dict(q)
|
| 1120 |
qd['image_filename'] = None
|
| 1121 |
all_questions.append(qd)
|
| 1122 |
+
|
| 1123 |
+
# Add bookmarked classified questions
|
| 1124 |
+
for q in bookmarked_classified:
|
| 1125 |
+
qid = f"classified_{q['id']}"
|
| 1126 |
+
if qid not in seen_ids:
|
| 1127 |
+
all_questions.append(dict(q))
|
| 1128 |
+
seen_ids.add(qid)
|
| 1129 |
+
|
| 1130 |
+
# Add session's own classified questions (the "View as Collection" part)
|
| 1131 |
+
for q in session_classified:
|
| 1132 |
+
qid = f"classified_{q['id']}"
|
| 1133 |
+
if qid not in seen_ids:
|
| 1134 |
+
all_questions.append(dict(q))
|
| 1135 |
+
seen_ids.add(qid)
|
| 1136 |
|
| 1137 |
# Group questions by topic
|
| 1138 |
topics = {}
|
|
|
|
| 1231 |
ORDER BY b.created_at
|
| 1232 |
""", (session_id, current_user.id)).fetchall()
|
| 1233 |
|
| 1234 |
+
# Get explicitly bookmarked classified questions
|
| 1235 |
+
bookmarked_classified = conn.execute("""
|
| 1236 |
+
SELECT q.id, q.actual_solution as correct_answer_index, q.marked_solution as user_answer_index,
|
| 1237 |
+
q.chapter as topic, q.subject, i.processed_filename, i.note_filename
|
| 1238 |
+
FROM neetprep_bookmarks b
|
| 1239 |
+
JOIN questions q ON CAST(b.neetprep_question_id AS INTEGER) = q.id
|
| 1240 |
+
LEFT JOIN images i ON q.image_id = i.id
|
| 1241 |
+
WHERE b.session_id = ? AND b.user_id = ? AND b.question_type = 'classified'
|
| 1242 |
+
ORDER BY b.created_at
|
| 1243 |
+
""", (session_id, current_user.id)).fetchall()
|
| 1244 |
+
|
| 1245 |
+
# Get ALL classified questions from the session directly
|
| 1246 |
+
session_classified = conn.execute("""
|
| 1247 |
+
SELECT q.id, q.actual_solution as correct_answer_index, q.marked_solution as user_answer_index,
|
| 1248 |
+
q.chapter as topic, q.subject, i.processed_filename, i.note_filename
|
| 1249 |
+
FROM questions q
|
| 1250 |
+
LEFT JOIN images i ON q.image_id = i.id
|
| 1251 |
+
WHERE q.session_id = ? AND q.subject IS NOT NULL AND q.subject != 'Unclassified'
|
| 1252 |
+
AND q.chapter IS NOT NULL AND q.chapter != 'Unclassified'
|
| 1253 |
+
ORDER BY q.chapter, q.id
|
| 1254 |
+
""", (session_id,)).fetchall()
|
| 1255 |
+
|
| 1256 |
+
conn.close()
|
| 1257 |
+
|
| 1258 |
+
seen_ids = set()
|
| 1259 |
+
|
| 1260 |
+
# Process neetprep questions
|
| 1261 |
for q in neetprep_questions:
|
| 1262 |
+
qid = f"neetprep_{q['id']}"
|
| 1263 |
+
if qid in seen_ids: continue
|
| 1264 |
+
seen_ids.add(qid)
|
| 1265 |
+
|
| 1266 |
try:
|
| 1267 |
html_content = f"""<html><head><meta charset="utf-8"></head><body>{q['question_text']}</body></html>"""
|
| 1268 |
img_filename = f"neetprep_{q['id']}.jpg"
|
|
|
|
| 1283 |
except Exception as e:
|
| 1284 |
current_app.logger.error(f"Failed to convert question {q['id']} to image: {e}")
|
| 1285 |
|
| 1286 |
+
# Helper to add classified question
|
| 1287 |
+
def add_classified(q):
|
| 1288 |
+
qid = f"classified_{q['id']}"
|
| 1289 |
+
if qid in seen_ids: return
|
| 1290 |
+
seen_ids.add(qid)
|
| 1291 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1292 |
if q['processed_filename']:
|
| 1293 |
all_questions.append({
|
| 1294 |
'image_path': f"/processed/{q['processed_filename']}",
|
|
|
|
| 1304 |
}
|
| 1305 |
})
|
| 1306 |
|
| 1307 |
+
# Process bookmarked classified questions
|
| 1308 |
+
for q in bookmarked_classified:
|
| 1309 |
+
add_classified(q)
|
| 1310 |
+
|
| 1311 |
+
# Process session's own classified questions
|
| 1312 |
+
for q in session_classified:
|
| 1313 |
+
add_classified(q)
|
| 1314 |
|
| 1315 |
if not all_questions:
|
| 1316 |
from flask import redirect, flash
|
|
|
|
| 1342 |
ORDER BY nq.topic, b.created_at
|
| 1343 |
""", (session_id, current_user.id)).fetchall()
|
| 1344 |
|
| 1345 |
+
# Get explicitly bookmarked classified questions
|
| 1346 |
+
bookmarked_classified = conn.execute("""
|
| 1347 |
SELECT q.id, q.question_text, q.actual_solution as correct_answer_index,
|
| 1348 |
q.chapter as topic, q.subject, i.processed_filename
|
| 1349 |
FROM neetprep_bookmarks b
|
|
|
|
| 1353 |
ORDER BY q.chapter, b.created_at
|
| 1354 |
""", (session_id, current_user.id)).fetchall()
|
| 1355 |
|
| 1356 |
+
# Get ALL classified questions from the session directly
|
| 1357 |
+
session_classified = conn.execute("""
|
| 1358 |
+
SELECT q.id, q.question_text, q.actual_solution as correct_answer_index,
|
| 1359 |
+
q.chapter as topic, q.subject, i.processed_filename
|
| 1360 |
+
FROM questions q
|
| 1361 |
+
LEFT JOIN images i ON q.image_id = i.id
|
| 1362 |
+
WHERE q.session_id = ? AND q.subject IS NOT NULL AND q.subject != 'Unclassified'
|
| 1363 |
+
AND q.chapter IS NOT NULL AND q.chapter != 'Unclassified'
|
| 1364 |
+
ORDER BY q.chapter, q.id
|
| 1365 |
+
""", (session_id,)).fetchall()
|
| 1366 |
+
|
| 1367 |
conn.close()
|
| 1368 |
|
| 1369 |
+
if not neetprep_questions and not bookmarked_classified and not session_classified:
|
| 1370 |
return jsonify({'error': 'No questions in this collection'}), 400
|
| 1371 |
|
| 1372 |
data = request.json or {}
|
| 1373 |
all_questions = []
|
| 1374 |
+
seen_ids = set()
|
| 1375 |
|
| 1376 |
+
# Process neetprep questions
|
| 1377 |
for q in neetprep_questions:
|
| 1378 |
+
qid = f"neetprep_{q['id']}"
|
| 1379 |
+
if qid in seen_ids: continue
|
| 1380 |
+
seen_ids.add(qid)
|
| 1381 |
+
|
| 1382 |
all_questions.append({
|
| 1383 |
"id": q['id'],
|
| 1384 |
"question_text": q['question_text'],
|
|
|
|
| 1394 |
}
|
| 1395 |
})
|
| 1396 |
|
| 1397 |
+
# Helper to add classified
|
| 1398 |
+
def add_classified(q):
|
| 1399 |
+
qid = f"classified_{q['id']}"
|
| 1400 |
+
if qid in seen_ids: return
|
| 1401 |
+
seen_ids.add(qid)
|
| 1402 |
+
|
| 1403 |
if q['processed_filename']:
|
| 1404 |
# Use absolute path for PDF generation
|
| 1405 |
abs_img_path = os.path.abspath(os.path.join(current_app.config['PROCESSED_FOLDER'], q['processed_filename']))
|
|
|
|
| 1417 |
}
|
| 1418 |
})
|
| 1419 |
|
| 1420 |
+
# Process bookmarked classified
|
| 1421 |
+
for q in bookmarked_classified:
|
| 1422 |
+
add_classified(q)
|
| 1423 |
+
|
| 1424 |
+
# Process session classified
|
| 1425 |
+
for q in session_classified:
|
| 1426 |
+
add_classified(q)
|
| 1427 |
+
|
| 1428 |
topics = list(set(q['custom_fields']['topic'] for q in all_questions if q['custom_fields'].get('topic')))
|
| 1429 |
|
| 1430 |
final_json_output = {
|
routes/questions.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import os
|
| 2 |
from flask import current_app, request, jsonify, render_template, flash, redirect, url_for
|
| 3 |
from .common import main_bp, get_db_connection, login_required, current_user
|
|
|
|
| 4 |
from processing import resize_image_if_needed, call_nim_ocr_api, extract_question_number_from_ocr_result
|
| 5 |
from strings import ROUTE_SAVE_QUESTIONS, ROUTE_EXTRACT_QUESTION_NUMBER, ROUTE_EXTRACT_ALL_QUESTION_NUMBERS, METHOD_POST
|
| 6 |
import requests
|
|
@@ -329,6 +330,9 @@ def update_question_classification_single():
|
|
| 329 |
VALUES (?, ?, '', ?, ?, 'unattempted')
|
| 330 |
''', (img_info['session_id'], image_id, subject, chapter))
|
| 331 |
|
|
|
|
|
|
|
|
|
|
| 332 |
conn.commit(); conn.close()
|
| 333 |
return jsonify({'success': True})
|
| 334 |
except Exception as e:
|
|
@@ -381,6 +385,9 @@ def save_questions():
|
|
| 381 |
VALUES (?, ?, ?, '', ?, ?, ?, ?, ?)
|
| 382 |
''', (session_id, q['image_id'], q['question_number'], q['status'], q.get('marked_solution', ''), q.get('actual_solution', ''), q.get('time_taken', ''), data.get('pdf_tags', '')))
|
| 383 |
|
|
|
|
|
|
|
|
|
|
| 384 |
conn.commit(); conn.close()
|
| 385 |
return jsonify({'success': True, 'message': 'Questions saved successfully.'})
|
| 386 |
|
|
@@ -426,6 +433,9 @@ def autosave_question():
|
|
| 426 |
VALUES (?, ?, ?, '', ?, ?, ?, '', '')
|
| 427 |
''', (session_id, image_id, question_number, status, marked_solution, actual_solution))
|
| 428 |
|
|
|
|
|
|
|
|
|
|
| 429 |
conn.commit()
|
| 430 |
conn.close()
|
| 431 |
return jsonify({'success': True})
|
|
@@ -544,8 +554,17 @@ def delete_question(image_id):
|
|
| 544 |
conn.close(); return jsonify({'error': 'Unauthorized'}), 403
|
| 545 |
image_info = conn.execute('SELECT session_id, filename, processed_filename FROM images WHERE id = ?', (image_id,)).fetchone()
|
| 546 |
if not image_info: conn.close(); return jsonify({'error': 'Question not found'}), 404
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 547 |
conn.execute('DELETE FROM questions WHERE image_id = ?', (image_id,))
|
| 548 |
conn.execute('DELETE FROM images WHERE id = ?', (image_id,))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 549 |
conn.commit(); conn.close()
|
| 550 |
return jsonify({'success': True})
|
| 551 |
except Exception as e: return jsonify({'error': str(e)}), 500
|
|
|
|
| 1 |
import os
|
| 2 |
from flask import current_app, request, jsonify, render_template, flash, redirect, url_for
|
| 3 |
from .common import main_bp, get_db_connection, login_required, current_user
|
| 4 |
+
from utils import sync_neetprep_collection
|
| 5 |
from processing import resize_image_if_needed, call_nim_ocr_api, extract_question_number_from_ocr_result
|
| 6 |
from strings import ROUTE_SAVE_QUESTIONS, ROUTE_EXTRACT_QUESTION_NUMBER, ROUTE_EXTRACT_ALL_QUESTION_NUMBERS, METHOD_POST
|
| 7 |
import requests
|
|
|
|
| 330 |
VALUES (?, ?, '', ?, ?, 'unattempted')
|
| 331 |
''', (img_info['session_id'], image_id, subject, chapter))
|
| 332 |
|
| 333 |
+
# Auto-convert and sync neetprep collection
|
| 334 |
+
sync_neetprep_collection(conn, session_id, current_user.id)
|
| 335 |
+
|
| 336 |
conn.commit(); conn.close()
|
| 337 |
return jsonify({'success': True})
|
| 338 |
except Exception as e:
|
|
|
|
| 385 |
VALUES (?, ?, ?, '', ?, ?, ?, ?, ?)
|
| 386 |
''', (session_id, q['image_id'], q['question_number'], q['status'], q.get('marked_solution', ''), q.get('actual_solution', ''), q.get('time_taken', ''), data.get('pdf_tags', '')))
|
| 387 |
|
| 388 |
+
# Auto-convert and sync neetprep collection
|
| 389 |
+
sync_neetprep_collection(conn, session_id, current_user.id)
|
| 390 |
+
|
| 391 |
conn.commit(); conn.close()
|
| 392 |
return jsonify({'success': True, 'message': 'Questions saved successfully.'})
|
| 393 |
|
|
|
|
| 433 |
VALUES (?, ?, ?, '', ?, ?, ?, '', '')
|
| 434 |
''', (session_id, image_id, question_number, status, marked_solution, actual_solution))
|
| 435 |
|
| 436 |
+
# Auto-convert and sync neetprep collection
|
| 437 |
+
sync_neetprep_collection(conn, session_id, current_user.id)
|
| 438 |
+
|
| 439 |
conn.commit()
|
| 440 |
conn.close()
|
| 441 |
return jsonify({'success': True})
|
|
|
|
| 554 |
conn.close(); return jsonify({'error': 'Unauthorized'}), 403
|
| 555 |
image_info = conn.execute('SELECT session_id, filename, processed_filename FROM images WHERE id = ?', (image_id,)).fetchone()
|
| 556 |
if not image_info: conn.close(); return jsonify({'error': 'Question not found'}), 404
|
| 557 |
+
# Get question ID to delete bookmarks
|
| 558 |
+
q_row = conn.execute('SELECT id FROM questions WHERE image_id = ?', (image_id,)).fetchone()
|
| 559 |
+
if q_row:
|
| 560 |
+
conn.execute('DELETE FROM neetprep_bookmarks WHERE neetprep_question_id = ? AND question_type = ?', (str(q_row['id']), 'classified'))
|
| 561 |
+
|
| 562 |
conn.execute('DELETE FROM questions WHERE image_id = ?', (image_id,))
|
| 563 |
conn.execute('DELETE FROM images WHERE id = ?', (image_id,))
|
| 564 |
+
|
| 565 |
+
# Auto-convert and sync neetprep collection
|
| 566 |
+
sync_neetprep_collection(conn, image_info['session_id'], current_user.id)
|
| 567 |
+
|
| 568 |
conn.commit(); conn.close()
|
| 569 |
return jsonify({'success': True})
|
| 570 |
except Exception as e: return jsonify({'error': str(e)}), 500
|
templates/_revision_notes.html
CHANGED
|
@@ -1,581 +1,213 @@
|
|
| 1 |
-
{#
|
| 2 |
-
Revision Notes Modal - Final Fix
|
| 3 |
-
Fixes: Stylus Logic (via Native Pointer Capture), Object Eraser, UX
|
| 4 |
-
#}
|
| 5 |
-
|
| 6 |
-
{# ===== STYLES ===== #}
|
| 7 |
<style>
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
background-color: #ffffff;
|
| 20 |
-
background-image: radial-gradient(#ced4da 1px, transparent 1px);
|
| 21 |
-
background-size: 24px 24px;
|
| 22 |
-
/* CRITICAL: Disables browser zooming/scrolling so Pen works */
|
| 23 |
-
touch-action: none;
|
| 24 |
-
cursor: crosshair;
|
| 25 |
-
position: relative;
|
| 26 |
-
}
|
| 27 |
-
|
| 28 |
-
/* --- Toolbar --- */
|
| 29 |
-
.notes-toolbar {
|
| 30 |
-
position: absolute;
|
| 31 |
-
top: 24px;
|
| 32 |
-
left: 50%;
|
| 33 |
-
transform: translateX(-50%);
|
| 34 |
-
display: flex;
|
| 35 |
-
align-items: center;
|
| 36 |
-
gap: 8px;
|
| 37 |
-
padding: 8px 16px;
|
| 38 |
-
border-radius: 100px;
|
| 39 |
-
z-index: 1060;
|
| 40 |
-
background: rgba(33, 37, 41, 0.9);
|
| 41 |
-
backdrop-filter: blur(12px);
|
| 42 |
-
-webkit-backdrop-filter: blur(12px);
|
| 43 |
-
border: 1px solid rgba(255, 255, 255, 0.15);
|
| 44 |
-
box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
| 45 |
-
}
|
| 46 |
-
|
| 47 |
-
/* --- Buttons --- */
|
| 48 |
-
.tool-btn {
|
| 49 |
-
width: 42px;
|
| 50 |
-
height: 42px;
|
| 51 |
-
border-radius: 50%;
|
| 52 |
-
border: none;
|
| 53 |
-
background: transparent;
|
| 54 |
-
color: rgba(255,255,255,0.6);
|
| 55 |
-
font-size: 1.2rem;
|
| 56 |
-
display: flex;
|
| 57 |
-
align-items: center;
|
| 58 |
-
justify-content: center;
|
| 59 |
-
transition: all 0.2s;
|
| 60 |
-
}
|
| 61 |
-
|
| 62 |
-
.tool-btn:hover {
|
| 63 |
-
background: rgba(255,255,255,0.15);
|
| 64 |
-
color: #fff;
|
| 65 |
-
transform: translateY(-2px);
|
| 66 |
-
}
|
| 67 |
-
|
| 68 |
-
.tool-btn.active {
|
| 69 |
-
background: var(--accent-primary, #0d6efd);
|
| 70 |
-
color: white;
|
| 71 |
-
box-shadow: 0 4px 15px rgba(13, 110, 253, 0.4);
|
| 72 |
-
transform: scale(1.1);
|
| 73 |
-
}
|
| 74 |
-
|
| 75 |
-
/* Stylus Mode Active State */
|
| 76 |
-
#btn-stylus.active {
|
| 77 |
-
background: #198754; /* Green */
|
| 78 |
-
box-shadow: 0 4px 15px rgba(25, 135, 84, 0.4);
|
| 79 |
-
color: white;
|
| 80 |
-
}
|
| 81 |
-
|
| 82 |
-
.sep { width: 1px; height: 24px; background: rgba(255,255,255,0.2); margin: 0 6px; }
|
| 83 |
-
|
| 84 |
-
.color-dot {
|
| 85 |
-
width: 26px; height: 26px; border-radius: 50%; border: 2px solid transparent; cursor: pointer;
|
| 86 |
-
}
|
| 87 |
-
.color-dot.active { border-color: #fff; transform: scale(1.2); }
|
| 88 |
-
|
| 89 |
-
/* --- Ref Panel --- */
|
| 90 |
-
#ref-panel {
|
| 91 |
-
position: absolute; bottom: 20px; right: 20px; width: 280px;
|
| 92 |
-
background: #2b3035; border-radius: 12px; padding: 10px;
|
| 93 |
-
z-index: 1050; box-shadow: 0 10px 30px rgba(0,0,0,0.3);
|
| 94 |
-
transition: transform 0.3s ease;
|
| 95 |
-
}
|
| 96 |
-
#ref-panel.collapsed { transform: translateY(150%); }
|
| 97 |
-
#ref-panel img { width: 100%; border-radius: 8px; border: 1px solid #495057; }
|
| 98 |
-
|
| 99 |
-
.status-badge {
|
| 100 |
-
position: absolute; bottom: 20px; left: 20px;
|
| 101 |
-
background: rgba(0,0,0,0.7); color: white;
|
| 102 |
-
padding: 5px 12px; border-radius: 20px;
|
| 103 |
-
font-family: monospace; font-size: 0.85rem; pointer-events: none;
|
| 104 |
-
}
|
| 105 |
</style>
|
| 106 |
|
| 107 |
-
{# ===== HTML ===== #}
|
| 108 |
<div class="modal fade" id="notesModal" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false">
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
<ul class="dropdown-menu dropdown-menu-dark">
|
| 138 |
-
<li><button class="dropdown-item" onclick="addShape('rect')">Rectangle</button></li>
|
| 139 |
-
<li><button class="dropdown-item" onclick="addShape('circle')">Circle</button></li>
|
| 140 |
-
<li><button class="dropdown-item" onclick="addShape('arrow')">Arrow</button></li>
|
| 141 |
-
<li><button class="dropdown-item" onclick="addText()">Text</button></li>
|
| 142 |
-
</ul>
|
| 143 |
-
</div>
|
| 144 |
-
|
| 145 |
-
<button class="tool-btn" id="btn-select" onclick="setTool('select')"><i class="fas fa-mouse-pointer"></i></button>
|
| 146 |
-
|
| 147 |
-
<div class="sep"></div>
|
| 148 |
-
|
| 149 |
-
<!-- Stylus Toggle -->
|
| 150 |
-
<button class="tool-btn" id="btn-stylus" onclick="toggleStylus()" title="Stylus Only Mode">
|
| 151 |
-
<i class="fas fa-pen-nib"></i>
|
| 152 |
-
</button>
|
| 153 |
-
|
| 154 |
-
<div class="sep"></div>
|
| 155 |
-
|
| 156 |
-
<button class="tool-btn" onclick="undo()"><i class="fas fa-undo"></i></button>
|
| 157 |
-
<button class="tool-btn text-success" onclick="saveNotes()"><i class="fas fa-check"></i></button>
|
| 158 |
-
<button class="tool-btn text-secondary" data-bs-dismiss="modal"><i class="fas fa-times"></i></button>
|
| 159 |
-
</div>
|
| 160 |
-
|
| 161 |
-
<!-- Canvas Wrapper -->
|
| 162 |
-
<div id="notes-canvas-wrapper">
|
| 163 |
-
<canvas id="notes-canvas"></canvas>
|
| 164 |
-
</div>
|
| 165 |
-
|
| 166 |
-
<!-- Ref Panel -->
|
| 167 |
-
<div id="ref-panel">
|
| 168 |
-
<div class="d-flex justify-content-between align-items-center mb-1">
|
| 169 |
-
<small class="text-white-50">Reference</small>
|
| 170 |
-
<button class="btn btn-sm btn-link text-white-50 p-0" onclick="document.getElementById('ref-panel').classList.add('collapsed')"><i class="fas fa-chevron-down"></i></button>
|
| 171 |
-
</div>
|
| 172 |
-
<img id="notes-ref-img" src="">
|
| 173 |
-
</div>
|
| 174 |
-
|
| 175 |
-
<button class="btn btn-dark rounded-circle shadow position-absolute bottom-0 end-0 m-3" onclick="document.getElementById('ref-panel').classList.remove('collapsed')">
|
| 176 |
-
<i class="fas fa-image"></i>
|
| 177 |
-
</button>
|
| 178 |
-
|
| 179 |
-
<div class="status-badge">
|
| 180 |
-
<span id="debug-pointer">Mode: Touch Draw</span>
|
| 181 |
-
</div>
|
| 182 |
-
</div>
|
| 183 |
-
</div>
|
| 184 |
-
</div>
|
| 185 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
|
| 187 |
-
{# ===== JAVASCRIPT ===== #}
|
| 188 |
<script>
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 235 |
-
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
|
| 279 |
-
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
|
| 283 |
-
|
| 284 |
-
|
| 285 |
-
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
}
|
| 344 |
-
});
|
| 345 |
-
|
| 346 |
-
canvas.on('mouse:move', function(opt) {
|
| 347 |
-
if (this.isDragging) {
|
| 348 |
-
const e = opt.e;
|
| 349 |
-
const vpt = this.viewportTransform;
|
| 350 |
-
const clientX = e.clientX || (e.touches && e.touches[0]?.clientX) || this.lastPosX;
|
| 351 |
-
const clientY = e.clientY || (e.touches && e.touches[0]?.clientY) || this.lastPosY;
|
| 352 |
-
vpt[4] += clientX - this.lastPosX;
|
| 353 |
-
vpt[5] += clientY - this.lastPosY;
|
| 354 |
-
this.requestRenderAll();
|
| 355 |
-
this.lastPosX = clientX;
|
| 356 |
-
this.lastPosY = clientY;
|
| 357 |
-
}
|
| 358 |
-
if (this.isErasing) {
|
| 359 |
-
deleteObjectUnderPointer(opt.e);
|
| 360 |
-
}
|
| 361 |
-
});
|
| 362 |
-
|
| 363 |
-
canvas.on('mouse:up', function() {
|
| 364 |
-
const wasPanning = isFingerPanning;
|
| 365 |
-
this.isDragging = false;
|
| 366 |
-
this.isErasing = false;
|
| 367 |
-
isFingerPanning = false;
|
| 368 |
-
|
| 369 |
-
// Restore drawing mode if needed (only if not just finished finger panning)
|
| 370 |
-
if ((currentTool === 'pencil' || currentTool === 'highlighter') && !wasPanning) {
|
| 371 |
-
if (!isStylusMode || lastPointerType === 'pen') {
|
| 372 |
-
canvas.isDrawingMode = true;
|
| 373 |
-
}
|
| 374 |
-
}
|
| 375 |
-
|
| 376 |
-
// In stylus mode, keep drawing disabled until next pen touch
|
| 377 |
-
if (isStylusMode && lastPointerType === 'touch') {
|
| 378 |
-
canvas.isDrawingMode = false;
|
| 379 |
-
}
|
| 380 |
-
|
| 381 |
-
saveState();
|
| 382 |
-
});
|
| 383 |
-
}
|
| 384 |
-
|
| 385 |
-
function deleteObjectUnderPointer(e) {
|
| 386 |
-
// findTarget requires pointer event coordinates
|
| 387 |
-
const target = canvas.findTarget(e, false);
|
| 388 |
-
if (target) {
|
| 389 |
-
canvas.remove(target);
|
| 390 |
-
canvas.requestRenderAll();
|
| 391 |
-
}
|
| 392 |
-
}
|
| 393 |
-
|
| 394 |
-
function setTool(name) {
|
| 395 |
-
currentTool = name;
|
| 396 |
-
document.querySelectorAll('.tool-btn').forEach(b => b.classList.remove('active'));
|
| 397 |
-
document.getElementById(`btn-${name}`)?.classList.add('active');
|
| 398 |
-
|
| 399 |
-
// Reset
|
| 400 |
-
canvas.isDrawingMode = false;
|
| 401 |
-
canvas.selection = false;
|
| 402 |
-
canvas.defaultCursor = 'default';
|
| 403 |
-
canvas.getObjects().forEach(o => { o.selectable = false; o.evented = false; });
|
| 404 |
-
|
| 405 |
-
if (name === 'pencil') {
|
| 406 |
-
// In stylus mode, keep drawing disabled until pen touch
|
| 407 |
-
// In touch mode, enable immediately
|
| 408 |
-
canvas.isDrawingMode = !isStylusMode;
|
| 409 |
-
canvas.freeDrawingBrush = new fabric.PencilBrush(canvas);
|
| 410 |
-
canvas.freeDrawingBrush.color = currentColor;
|
| 411 |
-
canvas.freeDrawingBrush.width = 3;
|
| 412 |
-
}
|
| 413 |
-
else if (name === 'highlighter') {
|
| 414 |
-
// In stylus mode, keep drawing disabled until pen touch
|
| 415 |
-
canvas.isDrawingMode = !isStylusMode;
|
| 416 |
-
canvas.freeDrawingBrush = new fabric.PencilBrush(canvas);
|
| 417 |
-
const c = hexToRgb(currentColor);
|
| 418 |
-
canvas.freeDrawingBrush.color = `rgba(${c.r},${c.g},${c.b},0.3)`;
|
| 419 |
-
canvas.freeDrawingBrush.width = 20;
|
| 420 |
-
}
|
| 421 |
-
else if (name === 'eraser') {
|
| 422 |
-
canvas.defaultCursor = 'crosshair';
|
| 423 |
-
// Objects must be evented to be found by findTarget
|
| 424 |
-
canvas.getObjects().forEach(o => o.evented = true);
|
| 425 |
-
}
|
| 426 |
-
else if (name === 'select') {
|
| 427 |
-
canvas.selection = true;
|
| 428 |
-
canvas.defaultCursor = 'move';
|
| 429 |
-
canvas.getObjects().forEach(o => { o.selectable = true; o.evented = true; });
|
| 430 |
-
}
|
| 431 |
-
}
|
| 432 |
-
|
| 433 |
-
function toggleStylus() {
|
| 434 |
-
isStylusMode = !isStylusMode;
|
| 435 |
-
const btn = document.getElementById('btn-stylus');
|
| 436 |
-
const debug = document.getElementById('debug-pointer');
|
| 437 |
-
|
| 438 |
-
if (isStylusMode) {
|
| 439 |
-
btn.classList.add('active');
|
| 440 |
-
debug.textContent = "Mode: Stylus Only (Fingers Pan)";
|
| 441 |
-
debug.style.color = "#20c997";
|
| 442 |
-
// Disable drawing mode - will be enabled on pen touch
|
| 443 |
-
if (currentTool === 'pencil' || currentTool === 'highlighter') {
|
| 444 |
-
canvas.isDrawingMode = false;
|
| 445 |
-
}
|
| 446 |
-
} else {
|
| 447 |
-
btn.classList.remove('active');
|
| 448 |
-
debug.textContent = "Mode: Touch Draw";
|
| 449 |
-
debug.style.color = "white";
|
| 450 |
-
// Re-enable drawing mode for touch
|
| 451 |
-
if (currentTool === 'pencil' || currentTool === 'highlighter') {
|
| 452 |
-
canvas.isDrawingMode = true;
|
| 453 |
-
}
|
| 454 |
-
}
|
| 455 |
-
}
|
| 456 |
-
|
| 457 |
-
function addShape(type) {
|
| 458 |
-
setTool('select');
|
| 459 |
-
const center = canvas.getVpCenter();
|
| 460 |
-
const opts = { left:center.x, top:center.y, fill:'transparent', stroke:currentColor, strokeWidth:3, originX:'center', originY:'center' };
|
| 461 |
-
let obj;
|
| 462 |
-
if(type==='rect') obj = new fabric.Rect({...opts, width:100, height:80});
|
| 463 |
-
if(type==='circle') obj = new fabric.Circle({...opts, radius:40});
|
| 464 |
-
if(type==='arrow') obj = new fabric.Path('M 0 0 L 100 0 M 90 -10 L 100 0 L 90 10', {...opts, fill:null});
|
| 465 |
-
|
| 466 |
-
if(obj) { canvas.add(obj); canvas.setActiveObject(obj); saveState(); }
|
| 467 |
-
}
|
| 468 |
-
|
| 469 |
-
function addText() {
|
| 470 |
-
setTool('select');
|
| 471 |
-
const center = canvas.getVpCenter();
|
| 472 |
-
const t = new fabric.IText('Text', { left:center.x, top:center.y, fontSize:24, fill:currentColor });
|
| 473 |
-
canvas.add(t); canvas.setActiveObject(t); t.enterEditing(); saveState();
|
| 474 |
-
}
|
| 475 |
-
|
| 476 |
-
function setColor(hex, el) {
|
| 477 |
-
currentColor = hex;
|
| 478 |
-
document.querySelectorAll('.color-dot').forEach(d => d.classList.remove('active'));
|
| 479 |
-
if(el) el.classList.add('active');
|
| 480 |
-
if(['pencil','highlighter'].includes(currentTool)) setTool(currentTool);
|
| 481 |
-
}
|
| 482 |
-
|
| 483 |
-
function setupZoom() {
|
| 484 |
-
canvas.on('mouse:wheel', function(opt) {
|
| 485 |
-
const delta = opt.e.deltaY;
|
| 486 |
-
let zoom = canvas.getZoom();
|
| 487 |
-
zoom *= 0.999 ** delta;
|
| 488 |
-
if (zoom > 5) zoom = 5; if (zoom < 0.2) zoom = 0.2;
|
| 489 |
-
canvas.zoomToPoint({ x: opt.e.offsetX, y: opt.e.offsetY }, zoom);
|
| 490 |
-
opt.e.preventDefault(); opt.e.stopPropagation();
|
| 491 |
-
});
|
| 492 |
-
}
|
| 493 |
-
|
| 494 |
-
function undo() {
|
| 495 |
-
if(historyStack.length <= 1) return;
|
| 496 |
-
historyStack.pop();
|
| 497 |
-
canvas.loadFromJSON(historyStack[historyStack.length-1], canvas.renderAll.bind(canvas));
|
| 498 |
-
}
|
| 499 |
-
|
| 500 |
-
function saveState() {
|
| 501 |
-
if(historyStack.length>10) historyStack.shift();
|
| 502 |
-
historyStack.push(JSON.stringify(canvas));
|
| 503 |
-
}
|
| 504 |
-
|
| 505 |
-
async function saveNotes() {
|
| 506 |
-
// Save as JSON AND rasterized PNG (needed for PDF/quiz display)
|
| 507 |
-
const jsonData = JSON.stringify(canvas.toJSON());
|
| 508 |
-
|
| 509 |
-
// Rasterize canvas to PNG data URL for PDF/quiz rendering
|
| 510 |
-
const imageData = canvas.toDataURL({ format: 'png', multiplier: 2 });
|
| 511 |
-
|
| 512 |
-
try {
|
| 513 |
-
const response = await fetch('/save_note_json', {
|
| 514 |
-
method: 'POST',
|
| 515 |
-
headers: { 'Content-Type': 'application/json' },
|
| 516 |
-
body: JSON.stringify({
|
| 517 |
-
image_id: activeImageId,
|
| 518 |
-
session_id: '{{ session_id }}',
|
| 519 |
-
json_data: jsonData,
|
| 520 |
-
image_data: imageData
|
| 521 |
-
})
|
| 522 |
-
});
|
| 523 |
-
|
| 524 |
-
const result = await response.json();
|
| 525 |
-
if (result.success) {
|
| 526 |
-
// Close modal
|
| 527 |
-
const modal = bootstrap.Modal.getInstance(document.getElementById('notesModal'));
|
| 528 |
-
modal.hide();
|
| 529 |
-
showStatus('Notes saved!', 'success');
|
| 530 |
-
// Reload page to update the UI state
|
| 531 |
-
setTimeout(() => location.reload(), 500);
|
| 532 |
-
} else {
|
| 533 |
-
showStatus('Error saving notes: ' + result.error, 'danger');
|
| 534 |
-
}
|
| 535 |
-
} catch (e) {
|
| 536 |
-
showStatus('Error: ' + e.message, 'danger');
|
| 537 |
-
}
|
| 538 |
-
}
|
| 539 |
-
|
| 540 |
-
function hexToRgb(hex) {
|
| 541 |
-
const r = parseInt(hex.slice(1,3),16), g = parseInt(hex.slice(3,5),16), b = parseInt(hex.slice(5,7),16);
|
| 542 |
-
return {r,g,b};
|
| 543 |
-
}
|
| 544 |
-
|
| 545 |
-
async function toggleNoteInPdf(imageId, include) {
|
| 546 |
-
try {
|
| 547 |
-
const response = await fetch('/toggle_note_in_pdf', {
|
| 548 |
-
method: 'POST',
|
| 549 |
-
headers: { 'Content-Type': 'application/json' },
|
| 550 |
-
body: JSON.stringify({ image_id: imageId, include: include })
|
| 551 |
-
});
|
| 552 |
-
const result = await response.json();
|
| 553 |
-
if (!result.success) {
|
| 554 |
-
showStatus('Failed to update setting: ' + result.error, 'danger');
|
| 555 |
-
}
|
| 556 |
-
} catch (e) {
|
| 557 |
-
showStatus('Error: ' + e.message, 'danger');
|
| 558 |
-
}
|
| 559 |
-
}
|
| 560 |
-
|
| 561 |
-
async function deleteNote(imageId) {
|
| 562 |
-
if (!confirm('Delete this note? This cannot be undone.')) return;
|
| 563 |
-
|
| 564 |
-
try {
|
| 565 |
-
const response = await fetch('/delete_note', {
|
| 566 |
-
method: 'POST',
|
| 567 |
-
headers: { 'Content-Type': 'application/json' },
|
| 568 |
-
body: JSON.stringify({ image_id: imageId })
|
| 569 |
-
});
|
| 570 |
-
const result = await response.json();
|
| 571 |
-
if (result.success) {
|
| 572 |
-
showStatus('Note deleted', 'success');
|
| 573 |
-
location.reload();
|
| 574 |
-
} else {
|
| 575 |
-
showStatus('Failed to delete note: ' + result.error, 'danger');
|
| 576 |
-
}
|
| 577 |
-
} catch (e) {
|
| 578 |
-
showStatus('Error: ' + e.message, 'danger');
|
| 579 |
-
}
|
| 580 |
-
}
|
| 581 |
</script>
|
|
|
|
| 1 |
+
{# Revision Notes Modal - v3 Compact #}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
<style>
|
| 3 |
+
#notesModal .modal-body{padding:0;overflow:hidden;user-select:none}
|
| 4 |
+
#ncw{width:100%;height:100%;background:#fff;background-image:radial-gradient(#ced4da 1px,transparent 1px);background-size:24px 24px;touch-action:none;cursor:crosshair;position:relative}
|
| 5 |
+
.ntb{position:absolute;top:16px;left:50%;transform:translateX(-50%);display:flex;align-items:center;gap:6px;padding:6px 14px;border-radius:99px;z-index:1060;background:rgba(33,37,41,.92);backdrop-filter:blur(10px);box-shadow:0 8px 24px rgba(0,0,0,.3)}
|
| 6 |
+
.tb{width:38px;height:38px;border-radius:50%;border:0;background:0 0;color:rgba(255,255,255,.6);font-size:1.1rem;display:flex;align-items:center;justify-content:center;transition:.15s}
|
| 7 |
+
.tb:hover{background:rgba(255,255,255,.15);color:#fff}.tb.on{background:var(--ap,#0d6efd);color:#fff;box-shadow:0 3px 12px rgba(13,110,253,.4);transform:scale(1.08)}
|
| 8 |
+
.tb.sty{background:#198754!important;box-shadow:0 3px 12px rgba(25,135,84,.4)!important}
|
| 9 |
+
.sp{width:1px;height:22px;background:rgba(255,255,255,.2);margin:0 4px}
|
| 10 |
+
.cd{width:24px;height:24px;border-radius:50%;border:2px solid transparent;cursor:pointer;transition:.15s}.cd.on{border-color:#fff;transform:scale(1.2)}
|
| 11 |
+
#rpnl{position:absolute;bottom:16px;right:16px;width:260px;background:#2b3035;border-radius:10px;padding:8px;z-index:1050;box-shadow:0 8px 24px rgba(0,0,0,.3);transition:transform .25s}.rpnl-h{transform:translateY(150%)}
|
| 12 |
+
#rpnl img{width:100%;border-radius:6px;border:1px solid #495057}
|
| 13 |
+
#dbg{position:absolute;bottom:16px;left:16px;background:rgba(0,0,0,.7);color:#fff;padding:4px 10px;border-radius:16px;font:.8rem monospace;pointer-events:none}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
</style>
|
| 15 |
|
|
|
|
| 16 |
<div class="modal fade" id="notesModal" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false">
|
| 17 |
+
<div class="modal-dialog modal-fullscreen"><div class="modal-content bg-dark"><div class="modal-body">
|
| 18 |
+
<div class="ntb">
|
| 19 |
+
<button class="tb on" data-t="pencil" title="Pencil"><i class="fas fa-pencil-alt"></i></button>
|
| 20 |
+
<button class="tb" data-t="highlighter" title="Highlighter"><i class="fas fa-highlighter"></i></button>
|
| 21 |
+
<button class="tb" data-t="eraser" title="Eraser"><i class="fas fa-eraser"></i></button>
|
| 22 |
+
<div class="sp"></div>
|
| 23 |
+
<div class="d-flex gap-1">
|
| 24 |
+
<div class="cd on" data-c="#212529" style="background:#212529"></div>
|
| 25 |
+
<div class="cd" data-c="#dc3545" style="background:#dc3545"></div>
|
| 26 |
+
<div class="cd" data-c="#0d6efd" style="background:#0d6efd"></div>
|
| 27 |
+
</div>
|
| 28 |
+
<div class="sp"></div>
|
| 29 |
+
<div class="dropdown">
|
| 30 |
+
<button class="tb" data-bs-toggle="dropdown"><i class="fas fa-shapes"></i></button>
|
| 31 |
+
<ul class="dropdown-menu dropdown-menu-dark">
|
| 32 |
+
<li><button class="dropdown-item" data-sh="rect">Rectangle</button></li>
|
| 33 |
+
<li><button class="dropdown-item" data-sh="circle">Circle</button></li>
|
| 34 |
+
<li><button class="dropdown-item" data-sh="arrow">Arrow</button></li>
|
| 35 |
+
<li><button class="dropdown-item" data-sh="text">Text</button></li>
|
| 36 |
+
</ul>
|
| 37 |
+
</div>
|
| 38 |
+
<button class="tb" data-t="select"><i class="fas fa-mouse-pointer"></i></button>
|
| 39 |
+
<div class="sp"></div>
|
| 40 |
+
<button class="tb" id="bsty" title="Stylus Mode"><i class="fas fa-pen-nib"></i></button>
|
| 41 |
+
<div class="sp"></div>
|
| 42 |
+
<button class="tb" id="bundo"><i class="fas fa-undo"></i></button>
|
| 43 |
+
<button class="tb text-success" id="bsave"><i class="fas fa-check"></i></button>
|
| 44 |
+
<button class="tb text-secondary" data-bs-dismiss="modal"><i class="fas fa-times"></i></button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
</div>
|
| 46 |
+
<div id="ncw"><canvas id="nc"></canvas></div>
|
| 47 |
+
<div id="rpnl">
|
| 48 |
+
<div class="d-flex justify-content-between align-items-center mb-1">
|
| 49 |
+
<small class="text-white-50">Reference</small>
|
| 50 |
+
<button class="btn btn-sm btn-link text-white-50 p-0" id="rpnl-hide"><i class="fas fa-chevron-down"></i></button>
|
| 51 |
+
</div>
|
| 52 |
+
<img id="nref" src="">
|
| 53 |
+
</div>
|
| 54 |
+
<button class="btn btn-dark rounded-circle shadow position-absolute bottom-0 end-0 m-3" id="rpnl-show"><i class="fas fa-image"></i></button>
|
| 55 |
+
<div id="dbg">Touch Draw</div>
|
| 56 |
+
</div></div></div></div>
|
| 57 |
|
|
|
|
| 58 |
<script>
|
| 59 |
+
(function(){
|
| 60 |
+
const $ = s => document.querySelector(s), $$ = s => document.querySelectorAll(s);
|
| 61 |
+
let C, tool='pencil', color='#212529', imgId, hist=[], styMode=false, ptrType='mouse', fingerPan=false;
|
| 62 |
+
|
| 63 |
+
window.openNotesModal = function(id, ref) {
|
| 64 |
+
imgId = id; $('#nref').src = ref;
|
| 65 |
+
$('#rpnl').classList.remove('rpnl-h');
|
| 66 |
+
const m = new bootstrap.Modal($('#notesModal'));
|
| 67 |
+
m.show();
|
| 68 |
+
$('#notesModal').addEventListener('shown.bs.modal', init, {once:true});
|
| 69 |
+
};
|
| 70 |
+
|
| 71 |
+
function init() {
|
| 72 |
+
C && C.dispose();
|
| 73 |
+
const w = $('#ncw');
|
| 74 |
+
w.addEventListener('pointerdown', e => { ptrType = e.pointerType; }, true);
|
| 75 |
+
C = new fabric.Canvas('nc', {
|
| 76 |
+
width: w.clientWidth, height: w.clientHeight,
|
| 77 |
+
backgroundColor:'#fff', isDrawingMode:true, selection:false,
|
| 78 |
+
preserveObjectStacking:true, perPixelTargetFind:true
|
| 79 |
+
});
|
| 80 |
+
wire(); zoom(); setT('pencil'); load();
|
| 81 |
+
window.onresize = () => { C.setWidth(w.clientWidth); C.setHeight(w.clientHeight); };
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
async function load() {
|
| 85 |
+
try {
|
| 86 |
+
const r = await fetch('/get_note_json/'+imgId);
|
| 87 |
+
if(r.ok){ const d=await r.json(); if(d.success&&d.json_data){
|
| 88 |
+
const j = typeof d.json_data==='string'?JSON.parse(d.json_data):d.json_data;
|
| 89 |
+
C.loadFromJSON(j,()=>{ C.renderAll(); snap(); }); return;
|
| 90 |
+
}}
|
| 91 |
+
} catch(e){}
|
| 92 |
+
snap();
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
function wire() {
|
| 96 |
+
C.on('path:created', o => {
|
| 97 |
+
if((styMode && ptrType!=='pen') || fingerPan){ C.remove(o.path); C.requestRenderAll(); }
|
| 98 |
+
});
|
| 99 |
+
C.on('mouse:down', function(o){
|
| 100 |
+
const e = o.e;
|
| 101 |
+
if(styMode && ptrType==='touch'){
|
| 102 |
+
fingerPan=true; this.isDrawingMode=false; this.selection=false;
|
| 103 |
+
this.isDragging=true; this.lastPosX=e.clientX||0; this.lastPosY=e.clientY||0; return;
|
| 104 |
+
}
|
| 105 |
+
if(styMode && ptrType==='pen' && (tool==='pencil'||tool==='highlighter')) this.isDrawingMode=true;
|
| 106 |
+
if(!styMode && e.touches && e.touches.length>1){ this.isDrawingMode=false; this.isDragging=true; return; }
|
| 107 |
+
if(tool==='eraser'){ this.isDrawingMode=false; this.isErasing=true; eraseAt(e); }
|
| 108 |
+
});
|
| 109 |
+
C.on('mouse:move', function(o){
|
| 110 |
+
if(this.isDragging){
|
| 111 |
+
const e=o.e, v=this.viewportTransform,
|
| 112 |
+
x=e.clientX||(e.touches&&e.touches[0]?.clientX)||this.lastPosX,
|
| 113 |
+
y=e.clientY||(e.touches&&e.touches[0]?.clientY)||this.lastPosY;
|
| 114 |
+
v[4]+=x-this.lastPosX; v[5]+=y-this.lastPosY;
|
| 115 |
+
this.requestRenderAll(); this.lastPosX=x; this.lastPosY=y;
|
| 116 |
+
}
|
| 117 |
+
if(this.isErasing) eraseAt(o.e);
|
| 118 |
+
});
|
| 119 |
+
C.on('mouse:up', function(){
|
| 120 |
+
const wasPan=fingerPan;
|
| 121 |
+
this.isDragging=false; this.isErasing=false; fingerPan=false;
|
| 122 |
+
if((tool==='pencil'||tool==='highlighter')&&!wasPan&&(!styMode||ptrType==='pen')) C.isDrawingMode=true;
|
| 123 |
+
if(styMode&&ptrType==='touch') C.isDrawingMode=false;
|
| 124 |
+
snap();
|
| 125 |
+
});
|
| 126 |
+
}
|
| 127 |
+
|
| 128 |
+
function eraseAt(e){ const t=C.findTarget(e,false); if(t){ C.remove(t); C.requestRenderAll(); }}
|
| 129 |
+
|
| 130 |
+
function setT(t){
|
| 131 |
+
tool=t;
|
| 132 |
+
$$('.tb[data-t]').forEach(b=>b.classList.toggle('on',b.dataset.t===t));
|
| 133 |
+
C.isDrawingMode=false; C.selection=false; C.defaultCursor='default';
|
| 134 |
+
C.getObjects().forEach(o=>{ o.selectable=false; o.evented=false; });
|
| 135 |
+
if(t==='pencil'||t==='highlighter'){
|
| 136 |
+
C.isDrawingMode=!styMode;
|
| 137 |
+
C.freeDrawingBrush=new fabric.PencilBrush(C);
|
| 138 |
+
if(t==='highlighter'){ const c=h2r(color); C.freeDrawingBrush.color=`rgba(${c.r},${c.g},${c.b},.3)`; C.freeDrawingBrush.width=20; }
|
| 139 |
+
else{ C.freeDrawingBrush.color=color; C.freeDrawingBrush.width=3; }
|
| 140 |
+
} else if(t==='eraser'){
|
| 141 |
+
C.defaultCursor='crosshair'; C.getObjects().forEach(o=>o.evented=true);
|
| 142 |
+
} else if(t==='select'){
|
| 143 |
+
C.selection=true; C.defaultCursor='move'; C.getObjects().forEach(o=>{ o.selectable=true; o.evented=true; });
|
| 144 |
+
}
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
function setC(hex,el){ color=hex; $$('.cd').forEach(d=>d.classList.toggle('on',d===el)); if(tool==='pencil'||tool==='highlighter') setT(tool); }
|
| 148 |
+
|
| 149 |
+
function togSty(){
|
| 150 |
+
styMode=!styMode;
|
| 151 |
+
const b=$('#bsty'), d=$('#dbg');
|
| 152 |
+
b.classList.toggle('on',styMode); b.classList.toggle('sty',styMode);
|
| 153 |
+
d.textContent=styMode?'Stylus Only':'Touch Draw'; d.style.color=styMode?'#20c997':'#fff';
|
| 154 |
+
if(tool==='pencil'||tool==='highlighter') C.isDrawingMode=!styMode;
|
| 155 |
+
}
|
| 156 |
+
|
| 157 |
+
function addSh(type){
|
| 158 |
+
setT('select'); const p=C.getVpCenter(),
|
| 159 |
+
o={left:p.x,top:p.y,fill:'transparent',stroke:color,strokeWidth:3,originX:'center',originY:'center'};
|
| 160 |
+
let s;
|
| 161 |
+
if(type==='rect') s=new fabric.Rect({...o,width:100,height:80});
|
| 162 |
+
if(type==='circle') s=new fabric.Circle({...o,radius:40});
|
| 163 |
+
if(type==='arrow') s=new fabric.Path('M0 0L100 0M90-10L100 0L90 10',{...o,fill:null});
|
| 164 |
+
if(type==='text'){ s=new fabric.IText('Text',{left:p.x,top:p.y,fontSize:24,fill:color}); C.add(s); C.setActiveObject(s); s.enterEditing(); snap(); return; }
|
| 165 |
+
if(s){ C.add(s); C.setActiveObject(s); snap(); }
|
| 166 |
+
}
|
| 167 |
+
|
| 168 |
+
function zoom(){
|
| 169 |
+
C.on('mouse:wheel',function(o){
|
| 170 |
+
let z=C.getZoom()*(.999**o.e.deltaY);
|
| 171 |
+
z=Math.min(5,Math.max(.2,z));
|
| 172 |
+
C.zoomToPoint({x:o.e.offsetX,y:o.e.offsetY},z);
|
| 173 |
+
o.e.preventDefault(); o.e.stopPropagation();
|
| 174 |
+
});
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
function snap(){ if(hist.length>20) hist.shift(); hist.push(JSON.stringify(C)); }
|
| 178 |
+
function undo(){ if(hist.length<=1) return; hist.pop(); C.loadFromJSON(hist[hist.length-1],C.renderAll.bind(C)); }
|
| 179 |
+
|
| 180 |
+
async function save(){
|
| 181 |
+
const json=JSON.stringify(C.toJSON()), img=C.toDataURL({format:'png',multiplier:2});
|
| 182 |
+
try{
|
| 183 |
+
const r=await fetch('/save_note_json',{method:'POST',headers:{'Content-Type':'application/json'},
|
| 184 |
+
body:JSON.stringify({image_id:imgId,session_id:'{{ session_id }}',json_data:json,image_data:img})});
|
| 185 |
+
const d=await r.json();
|
| 186 |
+
if(d.success){ bootstrap.Modal.getInstance($('#notesModal')).hide(); showStatus('Saved!','success'); setTimeout(()=>location.reload(),400); }
|
| 187 |
+
else showStatus('Error: '+d.error,'danger');
|
| 188 |
+
}catch(e){ showStatus('Error: '+e.message,'danger'); }
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
function h2r(h){ return{r:parseInt(h.slice(1,3),16),g:parseInt(h.slice(3,5),16),b:parseInt(h.slice(5,7),16)}; }
|
| 192 |
+
|
| 193 |
+
// Event delegation
|
| 194 |
+
document.addEventListener('click', e => {
|
| 195 |
+
const t=e.target.closest('[data-t]'); if(t&&C){ setT(t.dataset.t); return; }
|
| 196 |
+
const c=e.target.closest('.cd'); if(c&&C){ setC(c.dataset.c,c); return; }
|
| 197 |
+
const s=e.target.closest('[data-sh]'); if(s&&C){ addSh(s.dataset.sh); return; }
|
| 198 |
+
if(e.target.closest('#bsty')){ togSty(); return; }
|
| 199 |
+
if(e.target.closest('#bundo')){ undo(); return; }
|
| 200 |
+
if(e.target.closest('#bsave')){ save(); return; }
|
| 201 |
+
if(e.target.closest('#rpnl-hide')){ $('#rpnl').classList.add('rpnl-h'); return; }
|
| 202 |
+
if(e.target.closest('#rpnl-show')){ $('#rpnl').classList.remove('rpnl-h'); return; }
|
| 203 |
+
});
|
| 204 |
+
|
| 205 |
+
window.toggleNoteInPdf = async(id,inc)=>{
|
| 206 |
+
try{ const r=await fetch('/toggle_note_in_pdf',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({image_id:id,include:inc})}); const d=await r.json(); if(!d.success) showStatus('Error: '+d.error,'danger'); }catch(e){ showStatus(e.message,'danger'); }
|
| 207 |
+
};
|
| 208 |
+
window.deleteNote = async id=>{
|
| 209 |
+
if(!confirm('Delete this note?')) return;
|
| 210 |
+
try{ const r=await fetch('/delete_note',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({image_id:id})}); const d=await r.json(); if(d.success){ showStatus('Deleted','success'); location.reload(); } else showStatus(d.error,'danger'); }catch(e){ showStatus(e.message,'danger'); }
|
| 211 |
+
};
|
| 212 |
+
})();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
</script>
|
templates/dashboard.html
CHANGED
|
@@ -113,23 +113,23 @@
|
|
| 113 |
<td>
|
| 114 |
<div class="btn-group" role="group">
|
| 115 |
{% if session.session_type == 'neetprep_collection' %}
|
| 116 |
-
<a href="{{ url_for('neetprep_bp.view_collection', session_id=session.id) }}" class="btn btn-sm btn-warning text-dark"><i class="bi bi-
|
| 117 |
-
<
|
| 118 |
-
<button class="btn btn-sm btn-info toggle-persist-btn"
|
| 119 |
{% elif session.session_type == 'color_rm' %}
|
| 120 |
<a href="{{ url_for('main.color_rm_interface', session_id=session.id, image_index=0) }}" class="btn btn-sm btn-warning text-dark">Color RM</a>
|
| 121 |
<button class="btn btn-sm btn-info toggle-persist-btn">Toggle Persist</button>
|
| 122 |
-
{% if show_size %}
|
| 123 |
-
<button class="btn btn-sm btn-warning reduce-space-btn">Reduce Space</button>
|
| 124 |
-
{% endif %}
|
| 125 |
{% else %}
|
| 126 |
-
<a href="{{ url_for('main.question_entry_v2', session_id=session.id) }}" class="btn btn-sm btn-primary">View</a>
|
| 127 |
-
<a href="{{ url_for('main.crop_interface_v2', session_id=session.id, image_index=0) }}" class="btn btn-sm btn-secondary"
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
{
|
| 131 |
-
|
|
|
|
| 132 |
{% endif %}
|
|
|
|
|
|
|
| 133 |
{% endif %}
|
| 134 |
</div>
|
| 135 |
</td>
|
|
@@ -387,35 +387,6 @@ $(document).ready(function() {
|
|
| 387 |
}
|
| 388 |
});
|
| 389 |
});
|
| 390 |
-
|
| 391 |
-
// Duplicate session as collection
|
| 392 |
-
$('.duplicate-session-btn').on('click', function() {
|
| 393 |
-
const button = $(this);
|
| 394 |
-
const sessionId = button.data('session-id');
|
| 395 |
-
|
| 396 |
-
if (confirm('Duplicate this session as a NEETprep collection? Only classified questions (subject + topic) will be included.')) {
|
| 397 |
-
button.prop('disabled', true).html('<span class="spinner-border spinner-border-sm"></span>');
|
| 398 |
-
|
| 399 |
-
$.ajax({
|
| 400 |
-
url: `/neetprep/collections/duplicate/${sessionId}`,
|
| 401 |
-
type: 'POST',
|
| 402 |
-
success: function(response) {
|
| 403 |
-
if (response.success) {
|
| 404 |
-
alert(`Successfully duplicated! Created collection "${response.name}" with ${response.count} questions.`);
|
| 405 |
-
location.reload();
|
| 406 |
-
} else {
|
| 407 |
-
alert('Error duplicating session: ' + response.error);
|
| 408 |
-
button.prop('disabled', false).html('<i class="bi bi-copy"></i>');
|
| 409 |
-
}
|
| 410 |
-
},
|
| 411 |
-
error: function(xhr) {
|
| 412 |
-
const err = xhr.responseJSON ? xhr.responseJSON.error : 'Unknown error';
|
| 413 |
-
alert('Error duplicating session: ' + err);
|
| 414 |
-
button.prop('disabled', false).html('<i class="bi bi-copy"></i>');
|
| 415 |
-
}
|
| 416 |
-
});
|
| 417 |
-
}
|
| 418 |
-
});
|
| 419 |
});
|
| 420 |
</script>
|
| 421 |
{% endblock %}
|
|
|
|
| 113 |
<td>
|
| 114 |
<div class="btn-group" role="group">
|
| 115 |
{% if session.session_type == 'neetprep_collection' %}
|
| 116 |
+
<a href="{{ url_for('neetprep_bp.view_collection', session_id=session.id) }}" class="btn btn-sm btn-warning text-dark" title="View Collection"><i class="bi bi-eye-fill me-1"></i>View Collection</a>
|
| 117 |
+
<a href="{{ url_for('main.question_entry_v2', session_id=session.id) }}" class="btn btn-sm btn-primary" title="View Questions"><i class="bi bi-list-check me-1"></i>View</a>
|
| 118 |
+
<button class="btn btn-sm btn-info toggle-persist-btn" title="Toggle Persistence"><i class="bi bi-shield-lock"></i></button>
|
| 119 |
{% elif session.session_type == 'color_rm' %}
|
| 120 |
<a href="{{ url_for('main.color_rm_interface', session_id=session.id, image_index=0) }}" class="btn btn-sm btn-warning text-dark">Color RM</a>
|
| 121 |
<button class="btn btn-sm btn-info toggle-persist-btn">Toggle Persist</button>
|
|
|
|
|
|
|
|
|
|
| 122 |
{% else %}
|
| 123 |
+
<a href="{{ url_for('main.question_entry_v2', session_id=session.id) }}" class="btn btn-sm btn-primary" title="View Questions"><i class="bi bi-list-check me-1"></i>View</a>
|
| 124 |
+
<a href="{{ url_for('main.crop_interface_v2', session_id=session.id, image_index=0) }}" class="btn btn-sm btn-secondary" title="Crop"><i class="bi bi-pencil"></i></a>
|
| 125 |
+
|
| 126 |
+
{% if session.has_classified %}
|
| 127 |
+
<a href="{{ url_for('neetprep_bp.view_collection', session_id=session.id) }}" class="btn btn-sm btn-warning fw-bold text-dark shadow-sm border-0" style="background: linear-gradient(135deg, #ffcc33 0%, #ffb300 100%);" title="View as Collection">
|
| 128 |
+
<i class="bi bi-star-fill me-1"></i>Collection
|
| 129 |
+
</a>
|
| 130 |
{% endif %}
|
| 131 |
+
|
| 132 |
+
<button class="btn btn-sm btn-info toggle-persist-btn" title="Toggle Persistence"><i class="bi bi-shield-lock"></i></button>
|
| 133 |
{% endif %}
|
| 134 |
</div>
|
| 135 |
</td>
|
|
|
|
| 387 |
}
|
| 388 |
});
|
| 389 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
});
|
| 391 |
</script>
|
| 392 |
{% endblock %}
|
templates/question_entry_v2.html
CHANGED
|
@@ -95,8 +95,13 @@
|
|
| 95 |
color: #fff;
|
| 96 |
}
|
| 97 |
|
| 98 |
-
|
| 99 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 100 |
}
|
| 101 |
|
| 102 |
/* --- UNIFIED NOTE CARD STYLES --- */
|
|
@@ -172,15 +177,8 @@
|
|
| 172 |
|
| 173 |
<form id="questions-form">
|
| 174 |
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
<div class="alert alert-warning alert-dismissible fade show" role="alert">
|
| 178 |
-
<i class="bi bi-exclamation-triangle me-2"></i>
|
| 179 |
-
NVIDIA NIM OCR feature is not available. To enable automatic question number extraction, please set the <code>NVIDIA_API_KEY</code> environment variable.
|
| 180 |
-
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
| 181 |
-
</div>
|
| 182 |
-
{% else %}
|
| 183 |
-
<div class="mb-3">
|
| 184 |
<button id="auto-extract-all" class="btn btn-primary">
|
| 185 |
<span class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
|
| 186 |
<span class="extract-text">Auto Extract All Question Numbers</span>
|
|
@@ -189,11 +187,12 @@
|
|
| 189 |
<span class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
|
| 190 |
<span class="extract-text">Extract & Classify All</span>
|
| 191 |
</button>
|
| 192 |
-
|
| 193 |
-
|
|
|
|
|
|
|
| 194 |
</button>
|
| 195 |
</div>
|
| 196 |
-
{% endif %}
|
| 197 |
|
| 198 |
{% for image in images %}
|
| 199 |
<fieldset class="row mb-4 border-bottom pb-3" data-question-index="{{ loop.index0 }}" id="question-fieldset-{{ image.id }}">
|
|
@@ -529,10 +528,11 @@
|
|
| 529 |
<div class="mb-3">
|
| 530 |
<label class="form-label small text-muted mb-2">Subject</label>
|
| 531 |
<div id="subject_pills" class="d-flex flex-wrap gap-2">
|
| 532 |
-
<button type="button" class="btn btn-sm btn-outline-
|
| 533 |
-
<button type="button" class="btn btn-sm btn-outline-warning subject-pill" data-subject="Chemistry">Chemistry</button>
|
| 534 |
-
<button type="button" class="btn btn-sm btn-outline-
|
| 535 |
-
<button type="button" class="btn btn-sm btn-outline-
|
|
|
|
| 536 |
</div>
|
| 537 |
</div>
|
| 538 |
|
|
@@ -541,6 +541,9 @@
|
|
| 541 |
<label class="form-label small text-muted mb-2">Chapter / Topic</label>
|
| 542 |
<div class="input-group input-group-lg">
|
| 543 |
<input type="text" id="topic_input" class="form-control bg-dark text-white border-secondary" placeholder="Start typing or use AI...">
|
|
|
|
|
|
|
|
|
|
| 544 |
<button class="btn btn-info" type="button" id="btn_get_suggestion" onclick="getAiSuggestion()">
|
| 545 |
<span class="spinner-border spinner-border-sm d-none" role="status"></span>
|
| 546 |
<i class="bi bi-stars"></i>
|
|
@@ -550,6 +553,7 @@
|
|
| 550 |
|
| 551 |
<!-- Suggestions -->
|
| 552 |
<div id="ai_suggestion_container" class="d-none">
|
|
|
|
| 553 |
<div id="ai_suggestion_chips" class="d-flex flex-wrap gap-2"></div>
|
| 554 |
</div>
|
| 555 |
<div id="ai_error_msg" class="alert alert-danger d-none mt-2 py-2"></div>
|
|
@@ -1347,6 +1351,15 @@
|
|
| 1347 |
let selectedRangeMode = 'all'; // 'all', 'unclassified', 'wrong', 'unattempted', 'custom'
|
| 1348 |
let customRanges = []; // Array of {start, end, subject} for custom slider ranges
|
| 1349 |
let questionSubjectMap = new Map(); // Maps question index to pre-selected subject
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1350 |
|
| 1351 |
// Get question image URL by index
|
| 1352 |
function getQuestionImageUrl(index) {
|
|
@@ -1512,7 +1525,8 @@
|
|
| 1512 |
|
| 1513 |
// Subject detection keywords
|
| 1514 |
const SUBJECT_KEYWORDS = {
|
| 1515 |
-
'Biology': ['cell', 'dna', 'rna', 'protein', 'enzyme', 'mitosis', 'meiosis', 'chromosome', 'gene', 'photosynthesis', 'respiration', 'nucleus', 'cytoplasm', 'membrane', 'tissue', 'organ', 'species', 'evolution', 'ecology', 'bacteria', 'virus', 'plant', 'animal', 'blood', 'heart', 'kidney', 'liver', 'neuron', 'hormone', 'digestion', 'reproduction', 'inheritance', 'mutation', 'allele', 'phenotype', 'genotype', 'ecosystem', 'biodiversity', 'nitrogen cycle', 'carbon cycle', 'food chain', 'lymph', 'antibody', 'antigen', 'vaccine', 'pathogen', 'biomolecule', 'carbohydrate', 'lipid', 'amino acid', 'nucleotide', 'atp', 'chlorophyll', 'stomata', 'xylem', 'phloem', 'transpiration', 'pollination', 'fertilization', 'embryo', 'zygote', 'gamete', 'ovary', 'testis', 'sperm', 'ovum', 'menstrual', 'placenta', 'umbilical', 'gestation', 'lactation', 'immunology', 'homeostasis'],
|
|
|
|
| 1516 |
'Chemistry': ['atom', 'molecule', 'ion', 'electron', 'proton', 'neutron', 'orbital', 'bond', 'covalent', 'ionic', 'metallic', 'oxidation', 'reduction', 'redox', 'acid', 'base', 'ph', 'salt', 'solution', 'concentration', 'mole', 'molarity', 'stoichiometry', 'equilibrium', 'catalyst', 'reaction', 'organic', 'inorganic', 'hydrocarbon', 'alkane', 'alkene', 'alkyne', 'alcohol', 'aldehyde', 'ketone', 'carboxylic', 'ester', 'amine', 'amide', 'polymer', 'isomer', 'electrolysis', 'electrochemical', 'thermodynamics', 'enthalpy', 'entropy', 'gibbs', 'periodic table', 'atomic number', 'mass number', 'isotope', 'valence', 'hybridization', 'resonance', 'aromaticity', 'benzene', 'phenol', 'ether', 'haloalkane', 'grignard', 'nucleophile', 'electrophile', 'sn1', 'sn2', 'elimination', 'addition', 'substitution', 'coordination', 'ligand', 'crystal field', 'lanthanide', 'actinide', 'd-block', 'p-block', 's-block', 'f-block', 'buffer', 'titration', 'indicator', 'solubility', 'precipitation', 'colligative', 'osmotic', 'vapour pressure', 'raoult', 'henry', 'nernst', 'faraday', 'electrochemistry', 'galvanic', 'electrolytic'],
|
| 1517 |
'Physics': ['force', 'mass', 'acceleration', 'velocity', 'momentum', 'energy', 'work', 'power', 'newton', 'gravity', 'friction', 'tension', 'torque', 'angular', 'rotational', 'oscillation', 'wave', 'frequency', 'wavelength', 'amplitude', 'sound', 'light', 'optics', 'lens', 'mirror', 'reflection', 'refraction', 'diffraction', 'interference', 'polarization', 'electric', 'current', 'voltage', 'resistance', 'capacitor', 'inductor', 'magnetic', 'electromagnetic', 'induction', 'transformer', 'generator', 'motor', 'circuit', 'ohm', 'kirchhoff', 'coulomb', 'gauss', 'ampere', 'faraday', 'lenz', 'maxwell', 'photoelectric', 'quantum', 'photon', 'electron', 'nucleus', 'radioactive', 'decay', 'fission', 'fusion', 'relativity', 'thermodynamics', 'heat', 'temperature', 'entropy', 'carnot', 'adiabatic', 'isothermal', 'isobaric', 'isochoric', 'kinetic theory', 'ideal gas', 'real gas', 'semiconductor', 'diode', 'transistor', 'logic gate', 'communication', 'modulation', 'satellite', 'doppler', 'spectrum', 'laser', 'holography', 'fibre optic', 'ray optics', 'wave optics', 'young', 'single slit', 'double slit', 'grating', 'brewster', 'malus', 'huygen'],
|
| 1518 |
'Mathematics': ['equation', 'function', 'derivative', 'integral', 'limit', 'matrix', 'vector', 'determinant', 'polynomial', 'quadratic', 'linear', 'differential', 'probability', 'statistics', 'mean', 'median', 'variance', 'standard deviation', 'permutation', 'combination', 'trigonometry', 'sine', 'cosine', 'tangent', 'logarithm', 'exponential', 'complex number', 'real number', 'set', 'relation', 'sequence', 'series', 'arithmetic progression', 'geometric progression', 'binomial', 'conic', 'parabola', 'ellipse', 'hyperbola', 'circle', 'straight line', 'plane', 'three dimensional', 'coordinate', 'calculus', 'continuity', 'differentiability', 'maxima', 'minima', 'area under curve', 'definite integral', 'indefinite integral', 'inverse trigonometric', 'mathematical induction', 'boolean algebra']
|
|
@@ -1521,6 +1535,7 @@
|
|
| 1521 |
// All NCERT chapters for autocomplete
|
| 1522 |
const ALL_CHAPTERS = {
|
| 1523 |
'Biology': ['The Living World', 'Biological Classification', 'Plant Kingdom', 'Animal Kingdom', 'Morphology of Flowering Plants', 'Anatomy of Flowering Plants', 'Structural Organisation in Animals', 'Cell: The Unit of Life', 'Biomolecules', 'Cell Cycle and Cell Division', 'Photosynthesis in Higher Plants', 'Respiration in Plants', 'Plant Growth and Development', 'Breathing and Exchange of Gases', 'Body Fluids and Circulation', 'Excretory Products and their Elimination', 'Locomotion and Movement', 'Neural Control and Coordination', 'Chemical Coordination and Integration', 'Reproduction in Organisms', 'Sexual Reproduction in Flowering Plants', 'Human Reproduction', 'Reproductive Health', 'Principles of Inheritance and Variation', 'Molecular Basis of Inheritance', 'Evolution', 'Human Health and Disease', 'Strategies for Enhancement in Food Production', 'Microbes in Human Welfare', 'Biotechnology: Principles and Processes', 'Biotechnology and its Applications', 'Organisms and Populations', 'Ecosystem', 'Biodiversity and Conservation', 'Environmental Issues', 'Transport in Plants', 'Mineral Nutrition', 'Digestion and Absorption'],
|
|
|
|
| 1524 |
'Chemistry': ['Some Basic Concepts of Chemistry', 'Structure of Atom', 'Classification of Elements and Periodicity in Properties', 'Chemical Bonding and Molecular Structure', 'Thermodynamics', 'Equilibrium', 'Redox Reactions', 'Organic Chemistry – Some Basic Principles and Techniques (GOC)', 'Hydrocarbons', 'Hydrogen', 'The s-Block Elements', 'The p-Block Elements', 'The d- and f-Block Elements', 'Coordination Compounds', 'Haloalkanes and Haloarenes', 'Alcohols, Phenols and Ethers', 'Aldehydes, Ketones and Carboxylic Acids', 'Amines', 'Biomolecules', 'Polymers', 'Chemistry in Everyday Life', 'Electrochemistry', 'Chemical Kinetics', 'Surface Chemistry', 'General Principles and Processes of Isolation of Elements', 'Solutions', 'Solid State', 'States of Matter'],
|
| 1525 |
'Physics': ['Physical World', 'Units and Measurements', 'Motion in a Straight Line', 'Motion in a Plane', 'Laws of Motion', 'Work, Energy and Power', 'System of Particles and Rotational Motion', 'Gravitation', 'Mechanical Properties of Solids', 'Mechanical Properties of Fluids', 'Thermal Properties of Matter', 'Thermodynamics', 'Kinetic Theory', 'Oscillations', 'Waves', 'Electric Charges and Fields', 'Electrostatic Potential and Capacitance', 'Current Electricity', 'Moving Charges and Magnetism', 'Magnetism and Matter', 'Electromagnetic Induction', 'Alternating Current', 'Electromagnetic Waves', 'Ray Optics and Optical Instruments', 'Wave Optics', 'Dual Nature of Radiation and Matter', 'Atoms', 'Nuclei', 'Semiconductor Electronics', 'Communication Systems'],
|
| 1526 |
'Mathematics': ['Sets', 'Relations and Functions', 'Trigonometric Functions', 'Complex Numbers and Quadratic Equations', 'Linear Inequalities', 'Permutations and Combinations', 'Binomial Theorem', 'Sequences and Series', 'Straight Lines', 'Conic Sections', 'Introduction to Three Dimensional Geometry', 'Limits and Derivatives', 'Statistics', 'Probability', 'Matrices', 'Determinants', 'Continuity and Differentiability', 'Applications of Derivatives', 'Integrals', 'Applications of Integrals', 'Differential Equations', 'Vector Algebra', 'Three Dimensional Geometry', 'Linear Programming', 'Inverse Trigonometric Functions', 'Mathematical Reasoning', 'Principle of Mathematical Induction']
|
|
@@ -1554,11 +1569,17 @@
|
|
| 1554 |
function setActiveSubject(subject) {
|
| 1555 |
manualSubject = subject;
|
| 1556 |
document.querySelectorAll('.subject-pill').forEach(pill => {
|
| 1557 |
-
pill.classList.remove('active', 'btn-success', 'btn-warning', 'btn-info', 'btn-danger');
|
| 1558 |
-
pill.classList.add('btn-outline-success', 'btn-outline-warning', 'btn-outline-info', 'btn-outline-danger');
|
| 1559 |
if (pill.dataset.subject === subject) {
|
| 1560 |
-
pill.classList.remove('btn-outline-success', 'btn-outline-warning', 'btn-outline-info', 'btn-outline-danger');
|
| 1561 |
-
const colorMap = {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1562 |
pill.classList.add('active', colorMap[subject] || 'btn-primary');
|
| 1563 |
}
|
| 1564 |
});
|
|
@@ -1871,10 +1892,16 @@
|
|
| 1871 |
document.getElementById('ai_suggestion_chips').innerHTML = '';
|
| 1872 |
document.getElementById('ai_error_msg').classList.add('d-none');
|
| 1873 |
|
| 1874 |
-
// Reset
|
| 1875 |
-
|
| 1876 |
-
|
| 1877 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1878 |
|
| 1879 |
// Auto-load suggestions if topic is empty
|
| 1880 |
if (!document.getElementById('topic_input').value) {
|
|
@@ -1937,6 +1964,12 @@
|
|
| 1937 |
const chipsContainer = document.getElementById('ai_suggestion_chips');
|
| 1938 |
const errorDiv = document.getElementById('ai_error_msg');
|
| 1939 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1940 |
btn.disabled = true;
|
| 1941 |
btn.querySelector('.spinner-border').classList.remove('d-none');
|
| 1942 |
errorDiv.classList.add('d-none');
|
|
@@ -1994,15 +2027,25 @@
|
|
| 1994 |
result.suggestions.forEach(suggestion => {
|
| 1995 |
const chip = document.createElement('button');
|
| 1996 |
chip.className = 'btn btn-outline-info btn-sm rounded-1';
|
| 1997 |
-
chip.
|
| 1998 |
chip.onclick = () => {
|
| 1999 |
document.getElementById('topic_input').value = suggestion;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2000 |
};
|
| 2001 |
chipsContainer.appendChild(chip);
|
| 2002 |
});
|
| 2003 |
|
| 2004 |
-
//
|
| 2005 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2006 |
if (!currentVal && result.suggestions.length > 0) {
|
| 2007 |
document.getElementById('topic_input').value = result.suggestions[0];
|
| 2008 |
}
|
|
@@ -2024,6 +2067,87 @@
|
|
| 2024 |
loadSettings();
|
| 2025 |
setupEventListeners();
|
| 2026 |
setupRangeToggles();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2027 |
});
|
| 2028 |
</script>
|
| 2029 |
{% endblock %}
|
|
|
|
| 95 |
color: #fff;
|
| 96 |
}
|
| 97 |
|
| 98 |
+
/* Highlighted chip for keyboard navigation */
|
| 99 |
+
#ai_suggestion_chips button.highlighted {
|
| 100 |
+
background-color: var(--accent-info);
|
| 101 |
+
color: var(--bg-dark);
|
| 102 |
+
border-color: #fff;
|
| 103 |
+
box-shadow: 0 0 8px var(--accent-info);
|
| 104 |
+
transform: translateY(-2px);
|
| 105 |
}
|
| 106 |
|
| 107 |
/* --- UNIFIED NOTE CARD STYLES --- */
|
|
|
|
| 177 |
|
| 178 |
<form id="questions-form">
|
| 179 |
|
| 180 |
+
<div class="mb-3 d-flex flex-wrap gap-2">
|
| 181 |
+
{% if nvidia_nim_available %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
<button id="auto-extract-all" class="btn btn-primary">
|
| 183 |
<span class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
|
| 184 |
<span class="extract-text">Auto Extract All Question Numbers</span>
|
|
|
|
| 187 |
<span class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
|
| 188 |
<span class="extract-text">Extract & Classify All</span>
|
| 189 |
</button>
|
| 190 |
+
{% endif %}
|
| 191 |
+
|
| 192 |
+
<button type="button" class="btn btn-warning" data-bs-toggle="modal" data-bs-target="#manualClassificationModal">
|
| 193 |
+
<i class="bi bi-list-check me-1"></i> Manual Classification
|
| 194 |
</button>
|
| 195 |
</div>
|
|
|
|
| 196 |
|
| 197 |
{% for image in images %}
|
| 198 |
<fieldset class="row mb-4 border-bottom pb-3" data-question-index="{{ loop.index0 }}" id="question-fieldset-{{ image.id }}">
|
|
|
|
| 528 |
<div class="mb-3">
|
| 529 |
<label class="form-label small text-muted mb-2">Subject</label>
|
| 530 |
<div id="subject_pills" class="d-flex flex-wrap gap-2">
|
| 531 |
+
<button type="button" class="btn btn-sm btn-outline-info subject-pill" data-subject="Physics">Physics (⇧1)</button>
|
| 532 |
+
<button type="button" class="btn btn-sm btn-outline-warning subject-pill" data-subject="Chemistry">Chemistry (⇧2)</button>
|
| 533 |
+
<button type="button" class="btn btn-sm btn-outline-success subject-pill" data-subject="Biology">Biology (⇧3)</button>
|
| 534 |
+
<button type="button" class="btn btn-sm btn-outline-primary subject-pill" data-subject="Zoology">Zoology (⇧4)</button>
|
| 535 |
+
<button type="button" class="btn btn-sm btn-outline-danger subject-pill" data-subject="Mathematics">Maths (⇧~)</button>
|
| 536 |
</div>
|
| 537 |
</div>
|
| 538 |
|
|
|
|
| 541 |
<label class="form-label small text-muted mb-2">Chapter / Topic</label>
|
| 542 |
<div class="input-group input-group-lg">
|
| 543 |
<input type="text" id="topic_input" class="form-control bg-dark text-white border-secondary" placeholder="Start typing or use AI...">
|
| 544 |
+
<button class="btn btn-outline-secondary" type="button" onclick="document.getElementById('topic_input').value=''; document.getElementById('topic_input').focus();">
|
| 545 |
+
<i class="bi bi-x-lg"></i>
|
| 546 |
+
</button>
|
| 547 |
<button class="btn btn-info" type="button" id="btn_get_suggestion" onclick="getAiSuggestion()">
|
| 548 |
<span class="spinner-border spinner-border-sm d-none" role="status"></span>
|
| 549 |
<i class="bi bi-stars"></i>
|
|
|
|
| 553 |
|
| 554 |
<!-- Suggestions -->
|
| 555 |
<div id="ai_suggestion_container" class="d-none">
|
| 556 |
+
<label class="form-label d-block small text-info mb-2 mt-1"><i class="bi bi-robot me-1"></i>AI Suggested:</label>
|
| 557 |
<div id="ai_suggestion_chips" class="d-flex flex-wrap gap-2"></div>
|
| 558 |
</div>
|
| 559 |
<div id="ai_error_msg" class="alert alert-danger d-none mt-2 py-2"></div>
|
|
|
|
| 1351 |
let selectedRangeMode = 'all'; // 'all', 'unclassified', 'wrong', 'unattempted', 'custom'
|
| 1352 |
let customRanges = []; // Array of {start, end, subject} for custom slider ranges
|
| 1353 |
let questionSubjectMap = new Map(); // Maps question index to pre-selected subject
|
| 1354 |
+
let activeSuggestionIndex = -1; // -1 means focus is in input
|
| 1355 |
+
|
| 1356 |
+
function updateHighlightedSuggestion() {
|
| 1357 |
+
const chips = document.querySelectorAll('#ai_suggestion_chips button');
|
| 1358 |
+
chips.forEach((chip, idx) => {
|
| 1359 |
+
if (idx === activeSuggestionIndex) chip.classList.add('highlighted');
|
| 1360 |
+
else chip.classList.remove('highlighted');
|
| 1361 |
+
});
|
| 1362 |
+
}
|
| 1363 |
|
| 1364 |
// Get question image URL by index
|
| 1365 |
function getQuestionImageUrl(index) {
|
|
|
|
| 1525 |
|
| 1526 |
// Subject detection keywords
|
| 1527 |
const SUBJECT_KEYWORDS = {
|
| 1528 |
+
'Biology': ['cell', 'dna', 'rna', 'protein', 'enzyme', 'mitosis', 'meiosis', 'chromosome', 'gene', 'photosynthesis', 'respiration', 'nucleus', 'cytoplasm', 'membrane', 'tissue', 'organ', 'species', 'evolution', 'ecology', 'bacteria', 'virus', 'plant', 'animal', 'blood', 'heart', 'kidney', 'liver', 'neuron', 'hormone', 'digestion', 'reproduction', 'inheritance', 'mutation', 'allele', 'phenotype', 'genotype', 'ecosystem', 'biodiversity', 'nitrogen cycle', 'carbon cycle', 'food chain', 'lymph', 'antibody', 'antigen', 'vaccine', 'pathogen', 'biomolecule', 'carbohydrate', 'lipid', 'amino acid', 'nucleotide', 'atp', 'chlorophyll', 'stomata', 'xylem', 'phloem', 'transpiration', 'pollination', 'fertilization', 'embryo', 'zygote', 'gamete', 'ovary', 'testis', ' sperm', 'ovum', 'menstrual', 'placenta', 'umbilical', 'gestation', 'lactation', 'immunology', 'homeostasis'],
|
| 1529 |
+
'Zoology': ['animal', 'human', 'mammal', 'reproduction', 'nervous system', 'muscular', 'skeletal', 'circulatory', 'digestive', 'respiratory', 'excretory', 'endocrine', 'evolution', 'genetics', 'biotechnology', 'health', 'disease', 'immunity', 'blood', 'heart', 'brain', 'hormones'],
|
| 1530 |
'Chemistry': ['atom', 'molecule', 'ion', 'electron', 'proton', 'neutron', 'orbital', 'bond', 'covalent', 'ionic', 'metallic', 'oxidation', 'reduction', 'redox', 'acid', 'base', 'ph', 'salt', 'solution', 'concentration', 'mole', 'molarity', 'stoichiometry', 'equilibrium', 'catalyst', 'reaction', 'organic', 'inorganic', 'hydrocarbon', 'alkane', 'alkene', 'alkyne', 'alcohol', 'aldehyde', 'ketone', 'carboxylic', 'ester', 'amine', 'amide', 'polymer', 'isomer', 'electrolysis', 'electrochemical', 'thermodynamics', 'enthalpy', 'entropy', 'gibbs', 'periodic table', 'atomic number', 'mass number', 'isotope', 'valence', 'hybridization', 'resonance', 'aromaticity', 'benzene', 'phenol', 'ether', 'haloalkane', 'grignard', 'nucleophile', 'electrophile', 'sn1', 'sn2', 'elimination', 'addition', 'substitution', 'coordination', 'ligand', 'crystal field', 'lanthanide', 'actinide', 'd-block', 'p-block', 's-block', 'f-block', 'buffer', 'titration', 'indicator', 'solubility', 'precipitation', 'colligative', 'osmotic', 'vapour pressure', 'raoult', 'henry', 'nernst', 'faraday', 'electrochemistry', 'galvanic', 'electrolytic'],
|
| 1531 |
'Physics': ['force', 'mass', 'acceleration', 'velocity', 'momentum', 'energy', 'work', 'power', 'newton', 'gravity', 'friction', 'tension', 'torque', 'angular', 'rotational', 'oscillation', 'wave', 'frequency', 'wavelength', 'amplitude', 'sound', 'light', 'optics', 'lens', 'mirror', 'reflection', 'refraction', 'diffraction', 'interference', 'polarization', 'electric', 'current', 'voltage', 'resistance', 'capacitor', 'inductor', 'magnetic', 'electromagnetic', 'induction', 'transformer', 'generator', 'motor', 'circuit', 'ohm', 'kirchhoff', 'coulomb', 'gauss', 'ampere', 'faraday', 'lenz', 'maxwell', 'photoelectric', 'quantum', 'photon', 'electron', 'nucleus', 'radioactive', 'decay', 'fission', 'fusion', 'relativity', 'thermodynamics', 'heat', 'temperature', 'entropy', 'carnot', 'adiabatic', 'isothermal', 'isobaric', 'isochoric', 'kinetic theory', 'ideal gas', 'real gas', 'semiconductor', 'diode', 'transistor', 'logic gate', 'communication', 'modulation', 'satellite', 'doppler', 'spectrum', 'laser', 'holography', 'fibre optic', 'ray optics', 'wave optics', 'young', 'single slit', 'double slit', 'grating', 'brewster', 'malus', 'huygen'],
|
| 1532 |
'Mathematics': ['equation', 'function', 'derivative', 'integral', 'limit', 'matrix', 'vector', 'determinant', 'polynomial', 'quadratic', 'linear', 'differential', 'probability', 'statistics', 'mean', 'median', 'variance', 'standard deviation', 'permutation', 'combination', 'trigonometry', 'sine', 'cosine', 'tangent', 'logarithm', 'exponential', 'complex number', 'real number', 'set', 'relation', 'sequence', 'series', 'arithmetic progression', 'geometric progression', 'binomial', 'conic', 'parabola', 'ellipse', 'hyperbola', 'circle', 'straight line', 'plane', 'three dimensional', 'coordinate', 'calculus', 'continuity', 'differentiability', 'maxima', 'minima', 'area under curve', 'definite integral', 'indefinite integral', 'inverse trigonometric', 'mathematical induction', 'boolean algebra']
|
|
|
|
| 1535 |
// All NCERT chapters for autocomplete
|
| 1536 |
const ALL_CHAPTERS = {
|
| 1537 |
'Biology': ['The Living World', 'Biological Classification', 'Plant Kingdom', 'Animal Kingdom', 'Morphology of Flowering Plants', 'Anatomy of Flowering Plants', 'Structural Organisation in Animals', 'Cell: The Unit of Life', 'Biomolecules', 'Cell Cycle and Cell Division', 'Photosynthesis in Higher Plants', 'Respiration in Plants', 'Plant Growth and Development', 'Breathing and Exchange of Gases', 'Body Fluids and Circulation', 'Excretory Products and their Elimination', 'Locomotion and Movement', 'Neural Control and Coordination', 'Chemical Coordination and Integration', 'Reproduction in Organisms', 'Sexual Reproduction in Flowering Plants', 'Human Reproduction', 'Reproductive Health', 'Principles of Inheritance and Variation', 'Molecular Basis of Inheritance', 'Evolution', 'Human Health and Disease', 'Strategies for Enhancement in Food Production', 'Microbes in Human Welfare', 'Biotechnology: Principles and Processes', 'Biotechnology and its Applications', 'Organisms and Populations', 'Ecosystem', 'Biodiversity and Conservation', 'Environmental Issues', 'Transport in Plants', 'Mineral Nutrition', 'Digestion and Absorption'],
|
| 1538 |
+
'Zoology': ['Animal Kingdom', 'Structural Organisation in Animals', 'Biomolecules', 'Breathing and Exchange of Gases', 'Body Fluids and Circulation', 'Excretory Products and their Elimination', 'Locomotion and Movement', 'Neural Control and Coordination', 'Chemical Coordination and Integration', 'Human Reproduction', 'Reproductive Health', 'Principles of Inheritance and Variation', 'Molecular Basis of Inheritance', 'Evolution', 'Human Health and Disease', 'Strategies for Enhancement in Food Production', 'Microbes in Human Welfare', 'Biotechnology: Principles and Processes', 'Biotechnology and its Applications', 'Organisms and Populations', 'Ecosystem', 'Biodiversity and Conservation', 'Environmental Issues'],
|
| 1539 |
'Chemistry': ['Some Basic Concepts of Chemistry', 'Structure of Atom', 'Classification of Elements and Periodicity in Properties', 'Chemical Bonding and Molecular Structure', 'Thermodynamics', 'Equilibrium', 'Redox Reactions', 'Organic Chemistry – Some Basic Principles and Techniques (GOC)', 'Hydrocarbons', 'Hydrogen', 'The s-Block Elements', 'The p-Block Elements', 'The d- and f-Block Elements', 'Coordination Compounds', 'Haloalkanes and Haloarenes', 'Alcohols, Phenols and Ethers', 'Aldehydes, Ketones and Carboxylic Acids', 'Amines', 'Biomolecules', 'Polymers', 'Chemistry in Everyday Life', 'Electrochemistry', 'Chemical Kinetics', 'Surface Chemistry', 'General Principles and Processes of Isolation of Elements', 'Solutions', 'Solid State', 'States of Matter'],
|
| 1540 |
'Physics': ['Physical World', 'Units and Measurements', 'Motion in a Straight Line', 'Motion in a Plane', 'Laws of Motion', 'Work, Energy and Power', 'System of Particles and Rotational Motion', 'Gravitation', 'Mechanical Properties of Solids', 'Mechanical Properties of Fluids', 'Thermal Properties of Matter', 'Thermodynamics', 'Kinetic Theory', 'Oscillations', 'Waves', 'Electric Charges and Fields', 'Electrostatic Potential and Capacitance', 'Current Electricity', 'Moving Charges and Magnetism', 'Magnetism and Matter', 'Electromagnetic Induction', 'Alternating Current', 'Electromagnetic Waves', 'Ray Optics and Optical Instruments', 'Wave Optics', 'Dual Nature of Radiation and Matter', 'Atoms', 'Nuclei', 'Semiconductor Electronics', 'Communication Systems'],
|
| 1541 |
'Mathematics': ['Sets', 'Relations and Functions', 'Trigonometric Functions', 'Complex Numbers and Quadratic Equations', 'Linear Inequalities', 'Permutations and Combinations', 'Binomial Theorem', 'Sequences and Series', 'Straight Lines', 'Conic Sections', 'Introduction to Three Dimensional Geometry', 'Limits and Derivatives', 'Statistics', 'Probability', 'Matrices', 'Determinants', 'Continuity and Differentiability', 'Applications of Derivatives', 'Integrals', 'Applications of Integrals', 'Differential Equations', 'Vector Algebra', 'Three Dimensional Geometry', 'Linear Programming', 'Inverse Trigonometric Functions', 'Mathematical Reasoning', 'Principle of Mathematical Induction']
|
|
|
|
| 1569 |
function setActiveSubject(subject) {
|
| 1570 |
manualSubject = subject;
|
| 1571 |
document.querySelectorAll('.subject-pill').forEach(pill => {
|
| 1572 |
+
pill.classList.remove('active', 'btn-success', 'btn-warning', 'btn-info', 'btn-danger', 'btn-primary');
|
| 1573 |
+
pill.classList.add('btn-outline-success', 'btn-outline-warning', 'btn-outline-info', 'btn-outline-danger', 'btn-outline-primary');
|
| 1574 |
if (pill.dataset.subject === subject) {
|
| 1575 |
+
pill.classList.remove('btn-outline-success', 'btn-outline-warning', 'btn-outline-info', 'btn-outline-danger', 'btn-outline-primary');
|
| 1576 |
+
const colorMap = {
|
| 1577 |
+
'Biology': 'btn-success',
|
| 1578 |
+
'Chemistry': 'btn-warning',
|
| 1579 |
+
'Physics': 'btn-info',
|
| 1580 |
+
'Zoology': 'btn-primary',
|
| 1581 |
+
'Mathematics': 'btn-danger'
|
| 1582 |
+
};
|
| 1583 |
pill.classList.add('active', colorMap[subject] || 'btn-primary');
|
| 1584 |
}
|
| 1585 |
});
|
|
|
|
| 1892 |
document.getElementById('ai_suggestion_chips').innerHTML = '';
|
| 1893 |
document.getElementById('ai_error_msg').classList.add('d-none');
|
| 1894 |
|
| 1895 |
+
// Reset keyboard navigation
|
| 1896 |
+
activeSuggestionIndex = -1;
|
| 1897 |
+
updateHighlightedSuggestion();
|
| 1898 |
+
|
| 1899 |
+
// Focus input after a small delay
|
| 1900 |
+
setTimeout(() => {
|
| 1901 |
+
const input = document.getElementById('topic_input');
|
| 1902 |
+
input.focus();
|
| 1903 |
+
input.select();
|
| 1904 |
+
}, 100);
|
| 1905 |
|
| 1906 |
// Auto-load suggestions if topic is empty
|
| 1907 |
if (!document.getElementById('topic_input').value) {
|
|
|
|
| 1964 |
const chipsContainer = document.getElementById('ai_suggestion_chips');
|
| 1965 |
const errorDiv = document.getElementById('ai_error_msg');
|
| 1966 |
|
| 1967 |
+
{% if not nvidia_nim_available %}
|
| 1968 |
+
container.classList.remove('d-none');
|
| 1969 |
+
chipsContainer.innerHTML = '<span class="text-muted small"><i class="bi bi-info-circle me-1"></i>AI suggestions unavailable (No API key). Use manual entry below.</span>';
|
| 1970 |
+
return;
|
| 1971 |
+
{% endif %}
|
| 1972 |
+
|
| 1973 |
btn.disabled = true;
|
| 1974 |
btn.querySelector('.spinner-border').classList.remove('d-none');
|
| 1975 |
errorDiv.classList.add('d-none');
|
|
|
|
| 2027 |
result.suggestions.forEach(suggestion => {
|
| 2028 |
const chip = document.createElement('button');
|
| 2029 |
chip.className = 'btn btn-outline-info btn-sm rounded-1';
|
| 2030 |
+
chip.innerHTML = `<i class="bi bi-plus-circle me-1"></i>${suggestion}`;
|
| 2031 |
chip.onclick = () => {
|
| 2032 |
document.getElementById('topic_input').value = suggestion;
|
| 2033 |
+
// Clear suggestion highlighting and focus input
|
| 2034 |
+
activeSuggestionIndex = -1;
|
| 2035 |
+
updateHighlightedSuggestion();
|
| 2036 |
+
const input = document.getElementById('topic_input');
|
| 2037 |
+
input.focus();
|
| 2038 |
+
// We don't call nextTopicQuestion() here to allow review/editing
|
| 2039 |
};
|
| 2040 |
chipsContainer.appendChild(chip);
|
| 2041 |
});
|
| 2042 |
|
| 2043 |
+
// Reset selection
|
| 2044 |
+
activeSuggestionIndex = -1;
|
| 2045 |
+
updateHighlightedSuggestion();
|
| 2046 |
+
|
| 2047 |
+
// ONLY auto-fill input if it's currently empty
|
| 2048 |
+
const currentVal = document.getElementById('topic_input').value.trim();
|
| 2049 |
if (!currentVal && result.suggestions.length > 0) {
|
| 2050 |
document.getElementById('topic_input').value = result.suggestions[0];
|
| 2051 |
}
|
|
|
|
| 2067 |
loadSettings();
|
| 2068 |
setupEventListeners();
|
| 2069 |
setupRangeToggles();
|
| 2070 |
+
|
| 2071 |
+
// Keyboard navigation for Topic Wizard
|
| 2072 |
+
let lastEnterTime = 0;
|
| 2073 |
+
document.addEventListener('keydown', (e) => {
|
| 2074 |
+
const modal = document.getElementById('topicSelectionModal');
|
| 2075 |
+
if (!modal.classList.contains('show')) return;
|
| 2076 |
+
|
| 2077 |
+
const chips = document.querySelectorAll('#ai_suggestion_chips button');
|
| 2078 |
+
const input = document.getElementById('topic_input');
|
| 2079 |
+
|
| 2080 |
+
// --- Suggestion Cycling Logic ---
|
| 2081 |
+
if (e.key === 'ArrowDown') {
|
| 2082 |
+
if (activeSuggestionIndex === -1 && chips.length > 0) {
|
| 2083 |
+
e.preventDefault();
|
| 2084 |
+
activeSuggestionIndex = 0;
|
| 2085 |
+
updateHighlightedSuggestion();
|
| 2086 |
+
}
|
| 2087 |
+
} else if (e.key === 'ArrowUp') {
|
| 2088 |
+
if (activeSuggestionIndex !== -1) {
|
| 2089 |
+
e.preventDefault();
|
| 2090 |
+
activeSuggestionIndex = -1;
|
| 2091 |
+
updateHighlightedSuggestion();
|
| 2092 |
+
input.focus();
|
| 2093 |
+
}
|
| 2094 |
+
} else if (e.key === 'ArrowRight' && activeSuggestionIndex !== -1) {
|
| 2095 |
+
e.preventDefault();
|
| 2096 |
+
activeSuggestionIndex = (activeSuggestionIndex + 1) % chips.length;
|
| 2097 |
+
updateHighlightedSuggestion();
|
| 2098 |
+
} else if (e.key === 'ArrowLeft' && activeSuggestionIndex !== -1) {
|
| 2099 |
+
e.preventDefault();
|
| 2100 |
+
activeSuggestionIndex = (activeSuggestionIndex - 1 + chips.length) % chips.length;
|
| 2101 |
+
updateHighlightedSuggestion();
|
| 2102 |
+
}
|
| 2103 |
+
|
| 2104 |
+
// Enter to save and next
|
| 2105 |
+
if (e.key === 'Enter') {
|
| 2106 |
+
e.preventDefault();
|
| 2107 |
+
const now = Date.now();
|
| 2108 |
+
|
| 2109 |
+
// Double Enter Fallback: If pressed twice within 300ms, always go next
|
| 2110 |
+
if (now - lastEnterTime < 300) {
|
| 2111 |
+
nextTopicQuestion();
|
| 2112 |
+
lastEnterTime = 0;
|
| 2113 |
+
return;
|
| 2114 |
+
}
|
| 2115 |
+
lastEnterTime = now;
|
| 2116 |
+
|
| 2117 |
+
if (activeSuggestionIndex !== -1 && chips[activeSuggestionIndex]) {
|
| 2118 |
+
// Enter 1: Select the suggestion (triggers the click handler we modified)
|
| 2119 |
+
chips[activeSuggestionIndex].click();
|
| 2120 |
+
} else {
|
| 2121 |
+
// Enter 2 (or just Enter if in input): Save and move to next question
|
| 2122 |
+
nextTopicQuestion();
|
| 2123 |
+
}
|
| 2124 |
+
}
|
| 2125 |
+
|
| 2126 |
+
// Shift + Number shortcuts for subjects (Android compatible)
|
| 2127 |
+
if (e.shiftKey && !e.ctrlKey && !e.altKey) {
|
| 2128 |
+
let targetSubject = null;
|
| 2129 |
+
if (e.key === '1' || e.key === '!') targetSubject = 'Physics';
|
| 2130 |
+
else if (e.key === '2' || e.key === '@') targetSubject = 'Chemistry';
|
| 2131 |
+
else if (e.key === '3' || e.key === '#') targetSubject = 'Biology';
|
| 2132 |
+
else if (e.key === '4' || e.key === '$') targetSubject = 'Zoology';
|
| 2133 |
+
else if (e.key === '`' || e.key === '~') targetSubject = 'Mathematics';
|
| 2134 |
+
|
| 2135 |
+
if (targetSubject) {
|
| 2136 |
+
e.preventDefault();
|
| 2137 |
+
setActiveSubject(targetSubject);
|
| 2138 |
+
// Clear and fetch fresh suggestions for the new subject
|
| 2139 |
+
input.value = '';
|
| 2140 |
+
input.focus();
|
| 2141 |
+
getAiSuggestion();
|
| 2142 |
+
}
|
| 2143 |
+
}
|
| 2144 |
+
|
| 2145 |
+
// Alt + S to trigger AI suggestion
|
| 2146 |
+
if (e.altKey && (e.key === 's' || e.key === 'S')) {
|
| 2147 |
+
e.preventDefault();
|
| 2148 |
+
getAiSuggestion();
|
| 2149 |
+
}
|
| 2150 |
+
});
|
| 2151 |
});
|
| 2152 |
</script>
|
| 2153 |
{% endblock %}
|
templates/templates/question_entry_v2.html
CHANGED
|
@@ -37,14 +37,7 @@
|
|
| 37 |
|
| 38 |
<form id="questions-form">
|
| 39 |
|
| 40 |
-
|
| 41 |
-
{% if not nvidia_nim_available %}
|
| 42 |
-
<div class="alert alert-warning alert-dismissible fade show" role="alert">
|
| 43 |
-
<i class="bi bi-exclamation-triangle me-2"></i>
|
| 44 |
-
NVIDIA NIM OCR feature is not available. To enable automatic question number extraction, please set the <code>NVIDIA_API_KEY</code> environment variable.
|
| 45 |
-
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
| 46 |
-
</div>
|
| 47 |
-
{% else %}
|
| 48 |
<div class="mb-3">
|
| 49 |
<button id="auto-extract-all" class="btn btn-primary">
|
| 50 |
<span class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
|
|
|
|
| 37 |
|
| 38 |
<form id="questions-form">
|
| 39 |
|
| 40 |
+
{% if nvidia_nim_available %}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
<div class="mb-3">
|
| 42 |
<button id="auto-extract-all" class="btn btn-primary">
|
| 43 |
<span class="spinner-border spinner-border-sm d-none" role="status" aria-hidden="true"></span>
|
utils.py
CHANGED
|
@@ -12,6 +12,37 @@ def get_db_connection():
|
|
| 12 |
conn.row_factory = sqlite3.Row
|
| 13 |
return conn
|
| 14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
def get_or_download_font(font_path="arial.ttf", font_size=50):
|
| 16 |
if not os.path.exists(font_path):
|
| 17 |
try:
|
|
|
|
| 12 |
conn.row_factory = sqlite3.Row
|
| 13 |
return conn
|
| 14 |
|
| 15 |
+
def sync_neetprep_collection(conn, session_id, user_id):
|
| 16 |
+
"""
|
| 17 |
+
Ensures all classified questions in a session are bookmarked for the neetprep view.
|
| 18 |
+
"""
|
| 19 |
+
# Check if session has any classified questions (subject AND chapter set)
|
| 20 |
+
classified_qs = conn.execute('''
|
| 21 |
+
SELECT id FROM questions
|
| 22 |
+
WHERE session_id = ? AND subject IS NOT NULL AND subject != 'Unclassified'
|
| 23 |
+
AND chapter IS NOT NULL AND chapter != 'Unclassified'
|
| 24 |
+
''', (session_id,)).fetchall()
|
| 25 |
+
|
| 26 |
+
if not classified_qs:
|
| 27 |
+
return False
|
| 28 |
+
|
| 29 |
+
# Sync bookmarks for all classified questions
|
| 30 |
+
for q in classified_qs:
|
| 31 |
+
q_id = str(q['id'])
|
| 32 |
+
# Check if already bookmarked
|
| 33 |
+
exists = conn.execute('''
|
| 34 |
+
SELECT id FROM neetprep_bookmarks
|
| 35 |
+
WHERE user_id = ? AND neetprep_question_id = ? AND question_type = 'classified'
|
| 36 |
+
''', (user_id, q_id)).fetchone()
|
| 37 |
+
|
| 38 |
+
if not exists:
|
| 39 |
+
conn.execute('''
|
| 40 |
+
INSERT INTO neetprep_bookmarks (user_id, neetprep_question_id, session_id, question_type)
|
| 41 |
+
VALUES (?, ?, ?, 'classified')
|
| 42 |
+
''', (user_id, q_id, session_id))
|
| 43 |
+
|
| 44 |
+
return True
|
| 45 |
+
|
| 46 |
def get_or_download_font(font_path="arial.ttf", font_size=50):
|
| 47 |
if not os.path.exists(font_path):
|
| 48 |
try:
|