Awab-Elsadig commited on
Commit
46354f6
·
1 Parent(s): 4044a32

feat: add live search by id/text + filter chips (type, difficulty, bloom, status, answer, duplicates)

Browse files
Files changed (1) hide show
  1. main.py +140 -13
main.py CHANGED
@@ -1804,19 +1804,35 @@ async def latest_questions_page(courseId: int, chapterId: Optional[int] = None,
1804
  # Build batch HTML
1805
  batches_html_parts = []
1806
  for key, qs in sorted_batches:
1807
- q_rows = "".join(
1808
- f"<tr{' class=\"dup-row\"' if q.get('id') in dup_ids else ''}>"
1809
- f"<td><span class='badge badge-gray'>{q.get('id')}</span>{'<span class=\"dup-marker\" title=\"Duplicate\">⚠</span>' if q.get('id') in dup_ids else ''}</td>"
1810
- f"<td>{type_badge(q.get('questionType'))}</td>"
1811
- f"<td>{diff_badge(q.get('difficulty'))}</td>"
1812
- f"<td><span class='badge badge-gray'>{html.escape(str(q.get('bloomLevel','') or ''))}</span></td>"
1813
- f"<td>{status_badge(q.get('status'))}</td>"
1814
- f"<td class='img-cell'>{img_tag(q)}</td>"
1815
- f"<td>{'<span class=\"badge badge-green\">yes</span>' if (q.get('expectedAnswerText') or '').strip() else '<span class=\"badge badge-gray\">no</span>'}</td>"
1816
- f"<td class='text-cell' title=\"{html.escape(q.get('questionText') or '')}\">{html.escape(trunc(q.get('questionText') or ''))}</td>"
1817
- f"</tr>"
1818
- for q in qs
1819
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1820
  has_dups = any(q.get("id") in dup_ids for q in qs)
1821
  dup_warn = ' <span class="badge badge-red" style="font-size:0.65rem;">has duplicates</span>' if has_dups else ""
1822
  batches_html_parts.append(f"""
@@ -1865,6 +1881,25 @@ async def latest_questions_page(courseId: int, chapterId: Optional[int] = None,
1865
  empty = "" if questions else f'<p class="muted">No questions found for course {courseId} ({chapter_label}).</p>'
1866
  generated = _dt.now(_tz.utc).strftime("%Y-%m-%d %I:%M %p UTC")
1867
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1868
  return f"""<!doctype html>
1869
  <html lang="en"><head>
1870
  <meta charset="utf-8">
@@ -1904,6 +1939,23 @@ async def latest_questions_page(courseId: int, chapterId: Optional[int] = None,
1904
  .filter-row input[type=number]::-webkit-outer-spin-button,
1905
  .filter-row input[type=number]::-webkit-inner-spin-button {{ -webkit-appearance: none; margin: 0; }}
1906
  .filter-row input:focus {{ outline: none; border-color: #2563eb; }}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1907
  /* Summary strip */
1908
  .summary-strip {{ display: flex; flex-wrap: wrap; gap: 20px; }}
1909
  .summary-strip .stat {{ display: flex; flex-direction: column; }}
@@ -2027,6 +2079,22 @@ async def latest_questions_page(courseId: int, chapterId: Optional[int] = None,
2027
 
2028
  {empty}
2029
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2030
  <!-- Duplicates -->
2031
  <h2><i data-lucide="copy" style="width:12px;height:12px;display:inline;vertical-align:middle;margin-right:4px;"></i>Duplicates (exact text)</h2>
2032
  {dup_html}
@@ -2076,6 +2144,65 @@ async def latest_questions_page(courseId: int, chapterId: Optional[int] = None,
2076
 
2077
  // Lucide icons
2078
  lucide.createIcons();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2079
  </script>
2080
  </body></html>"""
2081
 
 
1804
  # Build batch HTML
1805
  batches_html_parts = []
1806
  for key, qs in sorted_batches:
1807
+ def _row(q):
1808
+ is_dup = q.get("id") in dup_ids
1809
+ has_ans = bool((q.get("expectedAnswerText") or "").strip())
1810
+ row_cls = "q-row dup-row" if is_dup else "q-row"
1811
+ dup_marker = '<span class="dup-marker" title="Duplicate">⚠</span>' if is_dup else ""
1812
+ ans_badge = '<span class="badge badge-green">yes</span>' if has_ans else '<span class="badge badge-gray">no</span>'
1813
+ qtext = q.get("questionText") or ""
1814
+ return (
1815
+ f"<tr class='{row_cls}'"
1816
+ f" data-id='{q.get('id')}'"
1817
+ f" data-type='{html.escape(str(q.get('questionType') or '').lower())}'"
1818
+ f" data-diff='{html.escape(str(q.get('difficulty') or '').lower())}'"
1819
+ f" data-bloom='{html.escape(str(q.get('bloomLevel') or '').lower())}'"
1820
+ f" data-status='{html.escape(str(q.get('status') or '').lower())}'"
1821
+ f" data-answer='{'yes' if has_ans else 'no'}'"
1822
+ f" data-dup='{'yes' if is_dup else 'no'}'"
1823
+ f" data-text='{html.escape(qtext.lower()[:400])}'"
1824
+ f">"
1825
+ f"<td><span class='badge badge-gray'>{q.get('id')}</span>{dup_marker}</td>"
1826
+ f"<td>{type_badge(q.get('questionType'))}</td>"
1827
+ f"<td>{diff_badge(q.get('difficulty'))}</td>"
1828
+ f"<td><span class='badge badge-gray'>{html.escape(str(q.get('bloomLevel', '') or ''))}</span></td>"
1829
+ f"<td>{status_badge(q.get('status'))}</td>"
1830
+ f"<td class='img-cell'>{img_tag(q)}</td>"
1831
+ f"<td>{ans_badge}</td>"
1832
+ f"<td class='text-cell' title='{html.escape(qtext)}'>{html.escape(trunc(qtext))}</td>"
1833
+ f"</tr>"
1834
+ )
1835
+ q_rows = "".join(_row(q) for q in qs)
1836
  has_dups = any(q.get("id") in dup_ids for q in qs)
1837
  dup_warn = ' <span class="badge badge-red" style="font-size:0.65rem;">has duplicates</span>' if has_dups else ""
1838
  batches_html_parts.append(f"""
 
1881
  empty = "" if questions else f'<p class="muted">No questions found for course {courseId} ({chapter_label}).</p>'
1882
  generated = _dt.now(_tz.utc).strftime("%Y-%m-%d %I:%M %p UTC")
1883
 
1884
+ # Distinct values for filter chips
1885
+ def _vals(key):
1886
+ return sorted({str(q.get(key) or "").strip() for q in questions if (q.get(key) or "").strip()})
1887
+ types_vals = _vals("questionType")
1888
+ diff_vals = _vals("difficulty")
1889
+ bloom_vals = _vals("bloomLevel")
1890
+ status_vals = _vals("status")
1891
+
1892
+ def chip_row(group_id, values, label):
1893
+ chips = "".join(
1894
+ f'<span class="chip" data-group="{html.escape(group_id)}" data-val="{html.escape(v)}" onclick="toggleChip(this)">{html.escape(v)}</span>'
1895
+ for v in values
1896
+ )
1897
+ return (
1898
+ f'<span class="chip-label">{html.escape(label)}</span>'
1899
+ f'{chips}'
1900
+ f'<span class="chip-sep"></span>'
1901
+ ) if values else ""
1902
+
1903
  return f"""<!doctype html>
1904
  <html lang="en"><head>
1905
  <meta charset="utf-8">
 
1939
  .filter-row input[type=number]::-webkit-outer-spin-button,
1940
  .filter-row input[type=number]::-webkit-inner-spin-button {{ -webkit-appearance: none; margin: 0; }}
1941
  .filter-row input:focus {{ outline: none; border-color: #2563eb; }}
1942
+ /* Live search + filter chips */
1943
+ .search-bar {{ width: 100%; padding: 7px 11px 7px 34px; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 0.85rem; font-family: inherit; background: #fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='14' height='14' viewBox='0 0 24 24' fill='none' stroke='%2394a3b8' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Ccircle cx='11' cy='11' r='8'/%3E%3Cline x1='21' y1='21' x2='16.65' y2='16.65'/%3E%3C/svg%3E") no-repeat 10px center; }}
1944
+ .search-bar:focus {{ outline: none; border-color: #2563eb; }}
1945
+ .chip-row {{ display: flex; gap: 6px; flex-wrap: wrap; margin-top: 10px; align-items: center; }}
1946
+ .chip-label {{ font-size: 0.68rem; font-weight: 700; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.06em; margin-right: 2px; }}
1947
+ .chip {{ padding: 3px 10px; border-radius: 9999px; font-size: 0.72rem; font-weight: 600; cursor: pointer; border: 1px solid #e2e8f0; background: #f8fafc; color: #475569; transition: background 0.1s, color 0.1s, border-color 0.1s; user-select: none; }}
1948
+ .chip:hover {{ background: #e2e8f0; }}
1949
+ .chip.active {{ background: #2563eb; color: #fff; border-color: #2563eb; }}
1950
+ .chip-sep {{ width: 1px; height: 18px; background: #e2e8f0; margin: 0 2px; }}
1951
+ .filter-count {{ font-size: 0.72rem; color: #64748b; margin-left: auto; }}
1952
+ body.dark .search-bar {{ background-color: #0f172a; border-color: #334155; color: #e2e8f0; }}
1953
+ body.dark .search-bar:focus {{ border-color: #2563eb; }}
1954
+ body.dark .chip {{ background: #1e293b; border-color: #334155; color: #94a3b8; }}
1955
+ body.dark .chip:hover {{ background: #334155; color: #e2e8f0; }}
1956
+ body.dark .chip.active {{ background: #2563eb; color: #fff; border-color: #2563eb; }}
1957
+ body.dark .chip-sep {{ background: #334155; }}
1958
+ body.dark .filter-count {{ color: #475569; }}
1959
  /* Summary strip */
1960
  .summary-strip {{ display: flex; flex-wrap: wrap; gap: 20px; }}
1961
  .summary-strip .stat {{ display: flex; flex-direction: column; }}
 
2079
 
2080
  {empty}
2081
 
2082
+ <!-- Live search + filter chips -->
2083
+ <div class="card" style="margin-bottom:16px;">
2084
+ <input class="search-bar" id="q-search" type="text" placeholder="Search by ID or question text…" oninput="applyFilters()">
2085
+ <div class="chip-row" id="chip-area">
2086
+ {chip_row("type", types_vals, "Type")}
2087
+ {chip_row("difficulty", diff_vals, "Difficulty")}
2088
+ {chip_row("bloom", bloom_vals, "Bloom")}
2089
+ {chip_row("status", status_vals, "Status")}
2090
+ <span class="chip" data-group="answer" data-val="yes" onclick="toggleChip(this)">Has answer</span>
2091
+ <span class="chip" data-group="answer" data-val="no" onclick="toggleChip(this)">No answer</span>
2092
+ <span class="chip-sep"></span>
2093
+ <span class="chip" data-group="dup" data-val="yes" onclick="toggleChip(this)">Duplicates only</span>
2094
+ <span class="filter-count" id="filter-count"></span>
2095
+ </div>
2096
+ </div>
2097
+
2098
  <!-- Duplicates -->
2099
  <h2><i data-lucide="copy" style="width:12px;height:12px;display:inline;vertical-align:middle;margin-right:4px;"></i>Duplicates (exact text)</h2>
2100
  {dup_html}
 
2144
 
2145
  // Lucide icons
2146
  lucide.createIcons();
2147
+
2148
+ // Live search + filter chips
2149
+ var activeChips = {{}}; // group -> Set of active values
2150
+
2151
+ function toggleChip(el) {{
2152
+ var group = el.dataset.group;
2153
+ var val = el.dataset.val;
2154
+ if (!activeChips[group]) activeChips[group] = new Set();
2155
+ if (activeChips[group].has(val)) {{
2156
+ activeChips[group].delete(val);
2157
+ el.classList.remove('active');
2158
+ }} else {{
2159
+ // For answer + dup groups only allow one active at a time
2160
+ if (group === 'answer' || group === 'dup') {{
2161
+ document.querySelectorAll('.chip[data-group="' + group + '"]').forEach(function(c) {{
2162
+ c.classList.remove('active');
2163
+ }});
2164
+ activeChips[group] = new Set();
2165
+ }}
2166
+ activeChips[group].add(val);
2167
+ el.classList.add('active');
2168
+ }}
2169
+ applyFilters();
2170
+ }}
2171
+
2172
+ function applyFilters() {{
2173
+ var search = (document.getElementById('q-search').value || '').toLowerCase().trim();
2174
+ var rows = document.querySelectorAll('tr.q-row');
2175
+ var visible = 0;
2176
+
2177
+ rows.forEach(function(row) {{
2178
+ // Search: match id or text
2179
+ var idMatch = !search || row.dataset.id.includes(search);
2180
+ var textMatch = !search || row.dataset.text.includes(search);
2181
+ if (!idMatch && !textMatch) {{ row.style.display = 'none'; return; }}
2182
+
2183
+ // Chip filters — each group is OR within, AND across groups
2184
+ for (var group in activeChips) {{
2185
+ if (!activeChips[group].size) continue;
2186
+ var field = row.dataset[group === 'bloom' ? 'bloom' : group]; // data-bloom etc
2187
+ if (!activeChips[group].has(field)) {{ row.style.display = 'none'; return; }}
2188
+ }}
2189
+
2190
+ row.style.display = '';
2191
+ visible++;
2192
+ }});
2193
+
2194
+ // Hide batch blocks that have no visible rows
2195
+ document.querySelectorAll('.batch-block').forEach(function(block) {{
2196
+ var anyVisible = Array.from(block.querySelectorAll('tr.q-row')).some(function(r) {{
2197
+ return r.style.display !== 'none';
2198
+ }});
2199
+ block.style.display = anyVisible ? '' : 'none';
2200
+ }});
2201
+
2202
+ var fc = document.getElementById('filter-count');
2203
+ if (fc) fc.textContent = (search || Object.values(activeChips).some(function(s){{return s.size;}}))
2204
+ ? visible + ' match' + (visible !== 1 ? 'es' : '') : '';
2205
+ }}
2206
  </script>
2207
  </body></html>"""
2208