Spaces:
Sleeping
Sleeping
Commit ·
46354f6
1
Parent(s): 4044a32
feat: add live search by id/text + filter chips (type, difficulty, bloom, status, answer, duplicates)
Browse files
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 |
-
|
| 1808 |
-
|
| 1809 |
-
|
| 1810 |
-
|
| 1811 |
-
|
| 1812 |
-
|
| 1813 |
-
|
| 1814 |
-
|
| 1815 |
-
|
| 1816 |
-
|
| 1817 |
-
|
| 1818 |
-
|
| 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 |
|