root commited on
Commit
4d801c0
·
1 Parent(s): 2cf90d8

feat: add session duplication as collection and flatten question entry UI

Browse files
neetprep.py CHANGED
@@ -873,6 +873,55 @@ def update_bookmark_collection(session_id):
873
  conn.close()
874
  return jsonify({'success': True})
875
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
876
  @neetprep_bp.route('/neetprep/bookmark', methods=['POST'])
877
  @login_required
878
  def add_bookmark():
 
873
  conn.close()
874
  return jsonify({'success': True})
875
 
876
+ @neetprep_bp.route('/neetprep/collections/duplicate/<source_session_id>', methods=['POST'])
877
+ @login_required
878
+ def duplicate_as_collection(source_session_id):
879
+ """Duplicate a session as a neetprep collection, including only classified questions."""
880
+ import uuid
881
+ conn = get_db_connection()
882
+
883
+ # 1. Verify source session ownership and get metadata
884
+ source_session = conn.execute('SELECT name, subject, tags, notes, original_filename FROM sessions WHERE id = ? AND user_id = ?', (source_session_id, current_user.id)).fetchone()
885
+ if not source_session:
886
+ conn.close()
887
+ return jsonify({'error': 'Source session not found'}), 404
888
+
889
+ # 2. Get all classified questions from the source session
890
+ classified_questions = conn.execute("""
891
+ SELECT id FROM questions
892
+ WHERE session_id = ? AND subject IS NOT NULL AND chapter IS NOT NULL AND chapter != 'Unclassified'
893
+ """, (source_session_id,)).fetchall()
894
+
895
+ if not classified_questions:
896
+ conn.close()
897
+ return jsonify({'error': 'No classified questions found in this session.'}), 400
898
+
899
+ # 3. Create a new collection session
900
+ new_session_id = str(uuid.uuid4())
901
+ new_name = f"Copy of {source_session['name'] or source_session['original_filename']}"
902
+
903
+ conn.execute("""
904
+ INSERT INTO sessions (id, name, subject, tags, notes, user_id, session_type, persist)
905
+ VALUES (?, ?, ?, ?, ?, ?, 'neetprep_collection', 1)
906
+ """, (new_session_id, new_name, source_session['subject'], source_session['tags'], source_session['notes'], current_user.id))
907
+
908
+ # 4. Link classified questions to the new collection
909
+ for q in classified_questions:
910
+ conn.execute("""
911
+ INSERT INTO neetprep_bookmarks (user_id, neetprep_question_id, session_id, question_type)
912
+ VALUES (?, ?, ?, 'classified')
913
+ """, (current_user.id, str(q['id']), new_session_id))
914
+
915
+ conn.commit()
916
+ conn.close()
917
+
918
+ return jsonify({
919
+ 'success': True,
920
+ 'session_id': new_session_id,
921
+ 'name': new_name,
922
+ 'count': len(classified_questions)
923
+ })
924
+
925
  @neetprep_bp.route('/neetprep/bookmark', methods=['POST'])
926
  @login_required
927
  def add_bookmark():
templates/dashboard.html CHANGED
@@ -125,6 +125,7 @@
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">Crop</a>
 
128
  <button class="btn btn-sm btn-info toggle-persist-btn">Toggle Persist</button>
129
  {% if show_size %}
130
  <button class="btn btn-sm btn-warning reduce-space-btn">Reduce Space</button>
@@ -386,6 +387,35 @@ $(document).ready(function() {
386
  }
387
  });
388
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
  });
390
  </script>
391
  {% endblock %}
 
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">Crop</a>
128
+ <button class="btn btn-sm btn-outline-success duplicate-session-btn" data-session-id="{{ session.id }}" title="Duplicate as NEETprep Collection"><i class="bi bi-copy"></i></button>
129
  <button class="btn btn-sm btn-info toggle-persist-btn">Toggle Persist</button>
130
  {% if show_size %}
131
  <button class="btn btn-sm btn-warning reduce-space-btn">Reduce Space</button>
 
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 %}
templates/question_entry_v2.html CHANGED
@@ -35,33 +35,32 @@
35
  border: 1px solid var(--border-subtle);
36
  background: transparent;
37
  color: #fff;
38
- border-radius: 20px;
39
  cursor: pointer;
40
  transition: all var(--transition-fast);
41
  }
42
  .status-btn.active { background: var(--accent-primary); border-color: var(--accent-primary); }
43
- .status-btn:hover { background: var(--bg-hover); transform: translateY(-1px); }
44
  .auto-extract-btn { min-width: 120px; }
45
 
46
  /* Subject pills */
47
  .subject-pill {
48
  transition: all var(--transition-fast);
49
- border-radius: 20px;
50
  }
51
  .subject-pill.active {
52
- transform: scale(1.05);
53
- box-shadow: 0 0 12px rgba(255,255,255,0.3);
54
  }
55
 
56
  /* Range toggles */
57
  .range-toggle {
58
  transition: all var(--transition-fast);
59
- border-radius: 20px;
60
  }
61
  .range-toggle:hover {
62
- transform: translateY(-1px);
63
  }
64
- .range-toggle.active { transform: scale(1.02); box-shadow: 0 0 8px rgba(255,255,255,0.2); }
65
  .range-toggle.active.btn-outline-primary { background: var(--accent-primary); color: #fff; }
66
  .range-toggle.active.btn-outline-warning { background: var(--accent-warning); color: #000; }
67
  .range-toggle.active.btn-outline-danger { background: var(--accent-danger); color: #fff; }
@@ -69,15 +68,15 @@
69
  .range-toggle.active.btn-outline-info { background: var(--accent-info); color: #000; }
70
 
71
  /* Range slider styling */
72
- .range-slider-row { background: var(--bg-elevated); border-radius: 10px; padding: 12px; margin-bottom: 10px; transition: all var(--transition-fast); }
73
  .range-slider-row:hover { background: var(--bg-hover); }
74
  .dual-range-container { position: relative; height: 40px; }
75
- .dual-range-track { position: absolute; top: 50%; left: 0; right: 0; height: 8px; background: var(--border-subtle); border-radius: 4px; transform: translateY(-50%); }
76
- .dual-range-highlight { position: absolute; top: 50%; height: 8px; background: linear-gradient(90deg, var(--accent-primary), var(--accent-info)); border-radius: 4px; transform: translateY(-50%); }
77
  .dual-range-container input[type="range"] { position: absolute; top: 0; left: 0; width: 100%; height: 100%; -webkit-appearance: none; background: transparent; pointer-events: none; }
78
- .dual-range-container input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 24px; height: 24px; background: #fff; border: 3px solid var(--accent-primary); border-radius: 50%; cursor: pointer; pointer-events: auto; box-shadow: var(--shadow-sm); transition: transform var(--transition-fast); }
79
- .dual-range-container input[type="range"]::-webkit-slider-thumb:hover { transform: scale(1.15); }
80
- .dual-range-container input[type="range"]::-moz-range-thumb { width: 24px; height: 24px; background: #fff; border: 3px solid var(--accent-primary); border-radius: 50%; cursor: pointer; pointer-events: auto; box-shadow: var(--shadow-sm); }
81
 
82
  /* Tom Select Dark Theme */
83
  .ts-wrapper .ts-control, .ts-wrapper .ts-control input {
@@ -102,26 +101,21 @@
102
 
103
  /* --- UNIFIED NOTE CARD STYLES --- */
104
  .note-card {
105
- background: linear-gradient(135deg, rgba(13, 202, 240, 0.08), rgba(13, 202, 240, 0.15));
106
- border: 1px solid rgba(13, 202, 240, 0.3);
107
- border-radius: 10px;
108
  padding: 10px;
109
  margin-bottom: 8px;
110
  transition: all var(--transition-fast);
111
  }
112
  .note-card:hover {
113
  border-color: var(--accent-info);
114
- box-shadow: 0 0 12px rgba(13, 202, 240, 0.15);
115
  }
116
  .note-thumbnail {
117
  max-height: 80px;
118
  object-fit: contain;
119
- border-radius: 6px;
120
- border: 1px solid rgba(13, 202, 240, 0.3);
121
- transition: transform var(--transition-fast);
122
- }
123
- .note-thumbnail:hover {
124
- transform: scale(1.05);
125
  }
126
  .note-actions {
127
  display: flex;
@@ -137,12 +131,11 @@
137
 
138
  /* --- UNIFIED BUTTON STYLES --- */
139
  .btn-pill {
140
- border-radius: 50px;
141
  font-weight: 500;
142
  transition: all var(--transition-fast);
143
  }
144
  .btn-pill:hover {
145
- transform: translateY(-1px);
146
  box-shadow: var(--shadow-sm);
147
  }
148
  </style>
@@ -421,7 +414,7 @@
421
  <div class="modal fade" id="manualClassificationModal" tabindex="-1">
422
  <div class="modal-dialog modal-lg">
423
  <div class="modal-content bg-dark text-white">
424
- <div class="modal-header border-secondary" style="background: linear-gradient(180deg, var(--bg-card), var(--bg-dark));">
425
  <h5 class="modal-title"><i class="bi bi-list-check me-2"></i>Quick Classification</h5>
426
  <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
427
  </div>
@@ -516,11 +509,11 @@
516
  <div class="modal fade" id="topicSelectionModal" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false">
517
  <div class="modal-dialog modal-lg modal-dialog-centered">
518
  <div class="modal-content bg-dark text-white border-secondary">
519
- <div class="modal-header border-secondary py-2" style="background: linear-gradient(180deg, var(--bg-card), var(--bg-dark));">
520
  <div class="d-flex align-items-center gap-3">
521
  <span class="badge bg-primary fs-6" id="topic_q_num">#1</span>
522
  <div class="progress flex-grow-1" style="width: 150px; height: 6px;">
523
- <div class="progress-bar" id="topic_progress_bar" role="progressbar" style="width: 0%; background: linear-gradient(90deg, var(--accent-primary), var(--accent-info));"></div>
524
  </div>
525
  <small id="topic_progress" class="text-muted">1/10</small>
526
  </div>
@@ -534,20 +527,12 @@
534
 
535
  <!-- Subject Selection (Auto-detected, editable) -->
536
  <div class="mb-3">
537
- <label class="form-label small text-muted mb-2">Subject (click to change)</label>
538
  <div id="subject_pills" class="d-flex flex-wrap gap-2">
539
- <button type="button" class="btn btn-outline-success subject-pill" data-subject="Biology">
540
- <i class="bi bi-flower1 me-1"></i>Biology
541
- </button>
542
- <button type="button" class="btn btn-outline-warning subject-pill" data-subject="Chemistry">
543
- <i class="bi bi-droplet me-1"></i>Chemistry
544
- </button>
545
- <button type="button" class="btn btn-outline-info subject-pill" data-subject="Physics">
546
- <i class="bi bi-lightning me-1"></i>Physics
547
- </button>
548
- <button type="button" class="btn btn-outline-danger subject-pill" data-subject="Mathematics">
549
- <i class="bi bi-calculator me-1"></i>Mathematics
550
- </button>
551
  </div>
552
  </div>
553
 
@@ -1599,7 +1584,7 @@
1599
  chipsContainer.innerHTML = '';
1600
  matches.forEach(suggestion => {
1601
  const chip = document.createElement('button');
1602
- chip.className = 'btn btn-outline-info btn-sm rounded-pill';
1603
  chip.innerText = suggestion;
1604
  chip.onclick = () => { input.value = suggestion; container.classList.add('d-none'); };
1605
  chipsContainer.appendChild(chip);
@@ -2008,7 +1993,7 @@
2008
 
2009
  result.suggestions.forEach(suggestion => {
2010
  const chip = document.createElement('button');
2011
- chip.className = 'btn btn-outline-info btn-sm rounded-pill';
2012
  chip.innerText = suggestion;
2013
  chip.onclick = () => {
2014
  document.getElementById('topic_input').value = suggestion;
 
35
  border: 1px solid var(--border-subtle);
36
  background: transparent;
37
  color: #fff;
38
+ border-radius: 4px;
39
  cursor: pointer;
40
  transition: all var(--transition-fast);
41
  }
42
  .status-btn.active { background: var(--accent-primary); border-color: var(--accent-primary); }
43
+ .status-btn:hover { background: var(--bg-hover); }
44
  .auto-extract-btn { min-width: 120px; }
45
 
46
  /* Subject pills */
47
  .subject-pill {
48
  transition: all var(--transition-fast);
49
+ border-radius: 4px;
50
  }
51
  .subject-pill.active {
52
+ box-shadow: 0 0 8px rgba(255,255,255,0.2);
 
53
  }
54
 
55
  /* Range toggles */
56
  .range-toggle {
57
  transition: all var(--transition-fast);
58
+ border-radius: 4px;
59
  }
60
  .range-toggle:hover {
61
+ background: var(--bg-hover);
62
  }
63
+ .range-toggle.active { box-shadow: 0 0 4px rgba(255,255,255,0.1); }
64
  .range-toggle.active.btn-outline-primary { background: var(--accent-primary); color: #fff; }
65
  .range-toggle.active.btn-outline-warning { background: var(--accent-warning); color: #000; }
66
  .range-toggle.active.btn-outline-danger { background: var(--accent-danger); color: #fff; }
 
68
  .range-toggle.active.btn-outline-info { background: var(--accent-info); color: #000; }
69
 
70
  /* Range slider styling */
71
+ .range-slider-row { background: var(--bg-elevated); border-radius: 4px; padding: 12px; margin-bottom: 10px; transition: all var(--transition-fast); }
72
  .range-slider-row:hover { background: var(--bg-hover); }
73
  .dual-range-container { position: relative; height: 40px; }
74
+ .dual-range-track { position: absolute; top: 50%; left: 0; right: 0; height: 8px; background: var(--border-subtle); border-radius: 2px; transform: translateY(-50%); }
75
+ .dual-range-highlight { position: absolute; top: 50%; height: 8px; background: var(--accent-primary); border-radius: 2px; transform: translateY(-50%); }
76
  .dual-range-container input[type="range"] { position: absolute; top: 0; left: 0; width: 100%; height: 100%; -webkit-appearance: none; background: transparent; pointer-events: none; }
77
+ .dual-range-container input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 16px; height: 24px; background: #fff; border: 2px solid var(--accent-primary); border-radius: 2px; cursor: pointer; pointer-events: auto; box-shadow: var(--shadow-sm); }
78
+ .dual-range-container input[type="range"]::-webkit-slider-thumb:hover { background: var(--accent-info); }
79
+ .dual-range-container input[type="range"]::-moz-range-thumb { width: 16px; height: 24px; background: #fff; border: 2px solid var(--accent-primary); border-radius: 2px; cursor: pointer; pointer-events: auto; box-shadow: var(--shadow-sm); }
80
 
81
  /* Tom Select Dark Theme */
82
  .ts-wrapper .ts-control, .ts-wrapper .ts-control input {
 
101
 
102
  /* --- UNIFIED NOTE CARD STYLES --- */
103
  .note-card {
104
+ background: rgba(13, 202, 240, 0.05);
105
+ border: 1px solid rgba(13, 202, 240, 0.2);
106
+ border-radius: 4px;
107
  padding: 10px;
108
  margin-bottom: 8px;
109
  transition: all var(--transition-fast);
110
  }
111
  .note-card:hover {
112
  border-color: var(--accent-info);
 
113
  }
114
  .note-thumbnail {
115
  max-height: 80px;
116
  object-fit: contain;
117
+ border-radius: 4px;
118
+ border: 1px solid rgba(13, 202, 240, 0.2);
 
 
 
 
119
  }
120
  .note-actions {
121
  display: flex;
 
131
 
132
  /* --- UNIFIED BUTTON STYLES --- */
133
  .btn-pill {
134
+ border-radius: 4px;
135
  font-weight: 500;
136
  transition: all var(--transition-fast);
137
  }
138
  .btn-pill:hover {
 
139
  box-shadow: var(--shadow-sm);
140
  }
141
  </style>
 
414
  <div class="modal fade" id="manualClassificationModal" tabindex="-1">
415
  <div class="modal-dialog modal-lg">
416
  <div class="modal-content bg-dark text-white">
417
+ <div class="modal-header border-secondary" style="background: var(--bg-card);">
418
  <h5 class="modal-title"><i class="bi bi-list-check me-2"></i>Quick Classification</h5>
419
  <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
420
  </div>
 
509
  <div class="modal fade" id="topicSelectionModal" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false">
510
  <div class="modal-dialog modal-lg modal-dialog-centered">
511
  <div class="modal-content bg-dark text-white border-secondary">
512
+ <div class="modal-header border-secondary py-2" style="background: var(--bg-card);">
513
  <div class="d-flex align-items-center gap-3">
514
  <span class="badge bg-primary fs-6" id="topic_q_num">#1</span>
515
  <div class="progress flex-grow-1" style="width: 150px; height: 6px;">
516
+ <div class="progress-bar" id="topic_progress_bar" role="progressbar" style="width: 0%; background: var(--accent-primary);"></div>
517
  </div>
518
  <small id="topic_progress" class="text-muted">1/10</small>
519
  </div>
 
527
 
528
  <!-- Subject Selection (Auto-detected, editable) -->
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-success subject-pill" data-subject="Biology">Biology</button>
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-info subject-pill" data-subject="Physics">Physics</button>
535
+ <button type="button" class="btn btn-sm btn-outline-danger subject-pill" data-subject="Mathematics">Maths</button>
 
 
 
 
 
 
 
 
536
  </div>
537
  </div>
538
 
 
1584
  chipsContainer.innerHTML = '';
1585
  matches.forEach(suggestion => {
1586
  const chip = document.createElement('button');
1587
+ chip.className = 'btn btn-outline-info btn-sm rounded-1';
1588
  chip.innerText = suggestion;
1589
  chip.onclick = () => { input.value = suggestion; container.classList.add('d-none'); };
1590
  chipsContainer.appendChild(chip);
 
1993
 
1994
  result.suggestions.forEach(suggestion => {
1995
  const chip = document.createElement('button');
1996
+ chip.className = 'btn btn-outline-info btn-sm rounded-1';
1997
  chip.innerText = suggestion;
1998
  chip.onclick = () => {
1999
  document.getElementById('topic_input').value = suggestion;