Spaces:
Sleeping
Sleeping
Commit ·
73e9d46
1
Parent(s): eb69985
Inspector: instant shell + animated loader, fix scrollbars and dark hint color
Browse files- Page loads immediately showing a bouncing books animation while data fetches
- Data fetched async via /latest-questions-data, injected with createContextualFragment
- Removed overflow-x scroll from card text/answer/hint, use word-break instead
- Fixed hint color in dark mode (#94a3b8 instead of near-invisible #475569)
main.py
CHANGED
|
@@ -1668,9 +1668,181 @@ def serve_output(filename: str):
|
|
| 1668 |
|
| 1669 |
|
| 1670 |
@app.get("/latest-questions", response_class=HTMLResponse)
|
| 1671 |
-
async def
|
| 1672 |
-
"""
|
| 1673 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1674 |
from collections import defaultdict
|
| 1675 |
from datetime import datetime as _dt, timezone as _tz
|
| 1676 |
|
|
@@ -1928,46 +2100,18 @@ async def latest_questions_page(courseId: int, chapterId: Optional[int] = None,
|
|
| 1928 |
f'<span class="chip-sep"></span>'
|
| 1929 |
) if values else ""
|
| 1930 |
|
| 1931 |
-
return f"""<
|
| 1932 |
-
<html lang="en"><head>
|
| 1933 |
-
<meta charset="utf-8">
|
| 1934 |
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 1935 |
-
<title>Latest Questions — course {courseId}</title>
|
| 1936 |
-
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css">
|
| 1937 |
-
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js"></script>
|
| 1938 |
-
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/contrib/auto-render.min.js"
|
| 1939 |
-
onload="renderMathInElement(document.body, {{ delimiters: [
|
| 1940 |
-
{{left: '$$', right: '$$', display: true}},
|
| 1941 |
-
{{left: '$', right: '$', display: false}}
|
| 1942 |
-
]}});"></script>
|
| 1943 |
-
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
| 1944 |
-
<style>
|
| 1945 |
-
*, *::before, *::after {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
| 1946 |
-
body {{ font-family: system-ui, sans-serif; background: #f8fafc; color: #1e293b; line-height: 1.5; }}
|
| 1947 |
-
.container {{ max-width: 1200px; margin: 0 auto; padding: 28px 16px 60px; }}
|
| 1948 |
-
h1 {{ font-size: 1.4rem; font-weight: 700; }}
|
| 1949 |
.subtitle {{ color: #64748b; margin: 3px 0 20px; font-size: 0.85rem; }}
|
| 1950 |
h2 {{ font-size: 0.8rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: #64748b; margin: 24px 0 10px; }}
|
| 1951 |
-
|
| 1952 |
-
.btn {{ padding: 8px 16px; border: none; border-radius: 6px; cursor: pointer; font-size: 0.85rem; font-weight: 600; font-family: inherit; transition: background 0.12s; display: inline-flex; align-items: center; gap: 6px; }}
|
| 1953 |
-
.btn-primary {{ background: #2563eb; color: white; }}
|
| 1954 |
.btn-primary:hover {{ background: #1d4ed8; }}
|
| 1955 |
-
.btn-secondary {{ background: #f1f5f9; color: #475569; border: 1px solid #e2e8f0; }}
|
| 1956 |
-
.btn-secondary:hover {{ background: #e2e8f0; }}
|
| 1957 |
-
.btn-sm {{ padding: 5px 12px; font-size: 0.78rem; }}
|
| 1958 |
-
/* Card */
|
| 1959 |
.card {{ background: white; border: 1px solid #e2e8f0; border-radius: 8px; padding: 16px 20px; margin-bottom: 10px; }}
|
| 1960 |
-
/* Dark mode toggle */
|
| 1961 |
-
.dm-toggle {{ display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 6px; border: 1px solid #e2e8f0; background: #f1f5f9; color: #475569; cursor: pointer; transition: background 0.15s, border-color 0.15s, color 0.15s; flex-shrink: 0; }}
|
| 1962 |
-
.dm-toggle:hover {{ background: #e2e8f0; color: #1e293b; }}
|
| 1963 |
-
/* Filter card */
|
| 1964 |
.filter-row {{ display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap; }}
|
| 1965 |
.filter-row label {{ display: flex; flex-direction: column; gap: 3px; font-size: 0.72rem; font-weight: 600; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em; }}
|
| 1966 |
.filter-row input[type=number] {{ width: 100px; padding: 6px 8px; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 0.85rem; font-family: inherit; -moz-appearance: textfield; appearance: textfield; }}
|
| 1967 |
.filter-row input[type=number]::-webkit-outer-spin-button,
|
| 1968 |
.filter-row input[type=number]::-webkit-inner-spin-button {{ -webkit-appearance: none; margin: 0; }}
|
| 1969 |
.filter-row input:focus {{ outline: none; border-color: #2563eb; }}
|
| 1970 |
-
/* Live search + filter chips */
|
| 1971 |
.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; }}
|
| 1972 |
.search-bar:focus {{ outline: none; border-color: #2563eb; }}
|
| 1973 |
.chip-row {{ display: flex; gap: 6px; flex-wrap: wrap; margin-top: 10px; align-items: center; }}
|
|
@@ -1977,87 +2121,60 @@ async def latest_questions_page(courseId: int, chapterId: Optional[int] = None,
|
|
| 1977 |
.chip.active {{ background: #2563eb; color: #fff; border-color: #2563eb; }}
|
| 1978 |
.chip-sep {{ width: 1px; height: 18px; background: #e2e8f0; margin: 0 2px; }}
|
| 1979 |
.filter-count {{ font-size: 0.72rem; color: #64748b; margin-left: auto; }}
|
| 1980 |
-
body.dark .search-bar {{ background-color: #0f172a; border-color: #334155; color: #e2e8f0; }}
|
| 1981 |
-
body.dark .search-bar:focus {{ border-color: #2563eb; }}
|
| 1982 |
-
body.dark .chip {{ background: #1e293b; border-color: #334155; color: #94a3b8; }}
|
| 1983 |
-
body.dark .chip:hover {{ background: #334155; color: #e2e8f0; }}
|
| 1984 |
-
body.dark .chip.active {{ background: #2563eb; color: #fff; border-color: #2563eb; }}
|
| 1985 |
-
body.dark .chip-sep {{ background: #334155; }}
|
| 1986 |
-
body.dark .filter-count {{ color: #475569; }}
|
| 1987 |
-
/* Summary strip */
|
| 1988 |
.summary-strip {{ display: flex; flex-wrap: wrap; gap: 20px; }}
|
| 1989 |
.summary-strip .stat {{ display: flex; flex-direction: column; }}
|
| 1990 |
.summary-strip .stat-val {{ font-size: 1.5rem; font-weight: 700; line-height: 1.1; }}
|
| 1991 |
.summary-strip .stat-lbl {{ font-size: 0.72rem; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em; }}
|
| 1992 |
-
/* Duplicate card */
|
| 1993 |
.dup-card {{ border-left: 3px solid #dc2626; }}
|
| 1994 |
.dup-count {{ display: inline-flex; align-items: center; gap: 5px; font-size: 0.72rem; font-weight: 700; color: #dc2626; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 8px; }}
|
| 1995 |
-
.qtext {{ font-size: 0.875rem; line-height: 1.7; margin-bottom: 10px; overflow-
|
| 1996 |
.meta-list {{ list-style: none; padding: 0; display: flex; flex-direction: column; gap: 4px; }}
|
| 1997 |
.meta-list li {{ font-size: 0.75rem; color: #64748b; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 5px; padding: 4px 8px; font-family: monospace; }}
|
| 1998 |
-
/* Badges */
|
| 1999 |
.badge {{ padding: 2px 7px; border-radius: 9999px; font-size: 0.65rem; font-weight: 600; white-space: nowrap; }}
|
| 2000 |
.badge-blue {{ background: #dbeafe; color: #1d4ed8; }}
|
| 2001 |
.badge-green {{ background: #dcfce7; color: #166534; }}
|
| 2002 |
.badge-gray {{ background: #f1f5f9; color: #64748b; }}
|
| 2003 |
.badge-red {{ background: #fee2e2; color: #dc2626; }}
|
| 2004 |
-
/* Batch groups */
|
| 2005 |
.batch-block {{ margin-bottom: 24px; }}
|
| 2006 |
.batch-header {{ display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }}
|
| 2007 |
.batch-meta {{ display: flex; align-items: center; gap: 7px; font-size: 0.78rem; flex-wrap: wrap; }}
|
| 2008 |
.batch-time {{ font-weight: 600; color: #1e293b; }}
|
| 2009 |
.batch-sep {{ color: #cbd5e1; }}
|
| 2010 |
-
body.dark .batch-time {{ color: #e2e8f0; }}
|
| 2011 |
-
body.dark .batch-sep {{ color: #334155; }}
|
| 2012 |
-
/* Cards grid */
|
| 2013 |
.cards-grid {{ display: flex; flex-direction: column; gap: 12px; }}
|
| 2014 |
-
/* Question card */
|
| 2015 |
.q-card {{ background: white; border: 1px solid #e2e8f0; border-radius: 10px; overflow: hidden; }}
|
| 2016 |
.dup-card-q {{ border-left: 3px solid #f59e0b; }}
|
| 2017 |
-
/* Top bar */
|
| 2018 |
.card-topbar {{ display: flex; align-items: center; gap: 6px; flex-wrap: wrap; padding: 10px 14px; border-bottom: 1px solid #f1f5f9; background: #f8fafc; }}
|
| 2019 |
.card-id {{ font-family: monospace; }}
|
| 2020 |
.card-ts {{ margin-left: auto; display: inline-flex; align-items: center; gap: 4px; font-size: 0.7rem; color: #94a3b8; white-space: nowrap; }}
|
| 2021 |
.dup-marker-badge {{ display: inline-flex; align-items: center; gap: 3px; font-size: 0.65rem; font-weight: 700; color: #d97706; background: #fef3c7; border: 1px solid #fde68a; border-radius: 9999px; padding: 1px 7px; }}
|
| 2022 |
-
/* Card body layouts */
|
| 2023 |
.card-body-split {{ display: grid; grid-template-columns: 1fr auto; gap: 16px; padding: 16px 14px; align-items: start; }}
|
| 2024 |
.card-body-single {{ padding: 16px 14px; }}
|
| 2025 |
-
.card-text {{ font-size: 0.9rem; line-height: 1.75; color: #1e293b; overflow-
|
| 2026 |
-
/* Image in card */
|
| 2027 |
.card-img-wrap {{ flex-shrink: 0; }}
|
| 2028 |
.card-img-wrap img {{ max-width: 220px; max-height: 180px; border: 1px solid #e2e8f0; border-radius: 7px; display: block; cursor: zoom-in; object-fit: contain; }}
|
| 2029 |
-
/* Answer / Hint sections */
|
| 2030 |
.card-section {{ padding: 10px 14px; border-top: 1px solid #f1f5f9; }}
|
| 2031 |
.section-label {{ display: block; font-size: 0.65rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; color: #94a3b8; margin-bottom: 4px; }}
|
| 2032 |
-
.card-ans {{ font-size: 0.82rem; line-height: 1.65; color: #1e293b; white-space: pre-wrap; overflow-
|
| 2033 |
-
.card-hint {{ font-size: 0.82rem; line-height: 1.65; color: #64748b; font-style: italic; white-space: pre-wrap; overflow-
|
| 2034 |
-
/* Dark mode cards */
|
| 2035 |
-
body.dark .q-card {{ background: #1e293b; border-color: #334155; }}
|
| 2036 |
-
body.dark .dup-card-q {{ border-left-color: #f59e0b; }}
|
| 2037 |
-
body.dark .card-topbar {{ background: #0f172a; border-bottom-color: #334155; }}
|
| 2038 |
-
body.dark .card-text {{ color: #e2e8f0; }}
|
| 2039 |
-
body.dark .card-section {{ border-top-color: #334155; }}
|
| 2040 |
-
body.dark .card-ans {{ color: #e2e8f0; }}
|
| 2041 |
-
body.dark .card-hint {{ color: #475569; }}
|
| 2042 |
-
body.dark .card-img-wrap img {{ border-color: #334155; }}
|
| 2043 |
-
/* Image modal */
|
| 2044 |
.img-modal {{ position: fixed; inset: 0; background: rgba(15,23,42,0.8); display: none; align-items: center; justify-content: center; z-index: 300; padding: 18px; }}
|
| 2045 |
.img-modal.open {{ display: flex; }}
|
| 2046 |
.img-modal img {{ max-width: min(1100px,96vw); max-height: 90vh; border-radius: 8px; box-shadow: 0 16px 40px rgba(15,23,42,0.45); background: white; object-fit: contain; }}
|
| 2047 |
.img-modal-close {{ position: absolute; top: 14px; right: 14px; border: 1px solid #94a3b8; background: #0f172a; color: white; border-radius: 6px; font-size: 0.78rem; padding: 6px 10px; cursor: pointer; }}
|
| 2048 |
.muted {{ color: #94a3b8; font-size: 0.78rem; }}
|
| 2049 |
/* Dark mode */
|
| 2050 |
-
body.dark {{ background: #0f172a; color: #e2e8f0; }}
|
| 2051 |
body.dark .card {{ background: #1e293b; border-color: #334155; }}
|
| 2052 |
-
body.dark .btn-secondary {{ background: #1e293b; color: #94a3b8; border-color: #334155; }}
|
| 2053 |
-
body.dark .btn-secondary:hover {{ background: #334155; color: #e2e8f0; }}
|
| 2054 |
-
body.dark .dm-toggle {{ background: #1e293b; border-color: #334155; color: #94a3b8; }}
|
| 2055 |
-
body.dark .dm-toggle:hover {{ background: #334155; color: #e2e8f0; }}
|
| 2056 |
body.dark .subtitle {{ color: #64748b; }}
|
| 2057 |
body.dark h2 {{ color: #475569; }}
|
| 2058 |
body.dark .filter-row label {{ color: #64748b; }}
|
| 2059 |
body.dark .filter-row input[type=number] {{ background: #0f172a; border-color: #334155; color: #e2e8f0; }}
|
| 2060 |
body.dark .filter-row input:focus {{ border-color: #2563eb; }}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2061 |
body.dark .summary-strip .stat-lbl {{ color: #475569; }}
|
| 2062 |
body.dark .dup-card {{ border-left-color: #f87171; }}
|
| 2063 |
body.dark .dup-count {{ color: #f87171; }}
|
|
@@ -2067,130 +2184,107 @@ async def latest_questions_page(courseId: int, chapterId: Optional[int] = None,
|
|
| 2067 |
body.dark .badge-gray {{ background: #334155; color: #94a3b8; }}
|
| 2068 |
body.dark .badge-red {{ background: #450a0a; color: #f87171; }}
|
| 2069 |
body.dark .muted {{ color: #475569; }}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2070 |
</style>
|
| 2071 |
-
</head>
|
| 2072 |
-
<body>
|
| 2073 |
-
<div class="container">
|
| 2074 |
|
| 2075 |
-
|
| 2076 |
-
|
| 2077 |
-
|
| 2078 |
-
|
| 2079 |
-
|
| 2080 |
-
|
| 2081 |
-
|
| 2082 |
-
|
| 2083 |
-
<
|
| 2084 |
-
|
| 2085 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2086 |
</button>
|
| 2087 |
</div>
|
| 2088 |
-
</
|
| 2089 |
-
|
| 2090 |
-
|
| 2091 |
-
<!-- Filter card -->
|
| 2092 |
-
<div class="card" style="margin-bottom:16px;">
|
| 2093 |
-
<form method="get" action="/latest-questions">
|
| 2094 |
-
<div class="filter-row">
|
| 2095 |
-
<label>Course ID
|
| 2096 |
-
<input type="number" name="courseId" value="{courseId}" required>
|
| 2097 |
-
</label>
|
| 2098 |
-
<label>Chapter ID (optional)
|
| 2099 |
-
<input type="number" name="chapterId" value="{chapterId if chapterId is not None else ''}">
|
| 2100 |
-
</label>
|
| 2101 |
-
<label>Limit
|
| 2102 |
-
<input type="number" name="limit" value="{limit}" min="1" max="500">
|
| 2103 |
-
</label>
|
| 2104 |
-
<button type="submit" class="btn btn-primary btn-sm" style="margin-bottom:1px;">
|
| 2105 |
-
<i data-lucide="refresh-cw" style="width:13px;height:13px;"></i>
|
| 2106 |
-
Refresh
|
| 2107 |
-
</button>
|
| 2108 |
-
</div>
|
| 2109 |
-
</form>
|
| 2110 |
-
</div>
|
| 2111 |
|
| 2112 |
-
|
| 2113 |
-
|
| 2114 |
-
|
| 2115 |
-
|
| 2116 |
-
|
| 2117 |
-
|
| 2118 |
-
|
| 2119 |
-
</div>
|
| 2120 |
</div>
|
|
|
|
| 2121 |
|
| 2122 |
-
|
| 2123 |
-
|
| 2124 |
-
|
| 2125 |
-
|
| 2126 |
-
|
| 2127 |
-
|
| 2128 |
-
|
| 2129 |
-
|
| 2130 |
-
|
| 2131 |
-
|
| 2132 |
-
|
| 2133 |
-
|
| 2134 |
-
|
| 2135 |
-
|
| 2136 |
-
|
| 2137 |
-
</div>
|
| 2138 |
</div>
|
|
|
|
| 2139 |
|
| 2140 |
-
|
| 2141 |
-
|
| 2142 |
-
|
| 2143 |
-
|
| 2144 |
-
<!-- Batch groups -->
|
| 2145 |
-
<h2><i data-lucide="layers" style="width:12px;height:12px;display:inline;vertical-align:middle;margin-right:4px;"></i>Save batches (grouped by minute)</h2>
|
| 2146 |
-
{batches_html}
|
| 2147 |
|
| 2148 |
-
<
|
|
|
|
|
|
|
| 2149 |
|
| 2150 |
-
<!-- Image zoom modal -->
|
| 2151 |
-
<
|
| 2152 |
-
<
|
| 2153 |
-
|
| 2154 |
-
<
|
|
|
|
|
|
|
| 2155 |
|
| 2156 |
<script>
|
| 2157 |
-
//
|
| 2158 |
-
function applyDark(dark) {{
|
| 2159 |
-
document.body.classList.toggle('dark', dark);
|
| 2160 |
-
document.getElementById('dm-icon-moon').style.display = dark ? 'none' : '';
|
| 2161 |
-
document.getElementById('dm-icon-sun').style.display = dark ? '' : 'none';
|
| 2162 |
-
}}
|
| 2163 |
-
function toggleDark() {{
|
| 2164 |
-
var isDark = document.body.classList.contains('dark');
|
| 2165 |
-
applyDark(!isDark);
|
| 2166 |
-
try {{ localStorage.setItem('dm', !isDark ? '1' : '0'); }} catch(e) {{}}
|
| 2167 |
-
}}
|
| 2168 |
(function() {{
|
| 2169 |
-
|
| 2170 |
-
|
| 2171 |
-
var prefersDark = saved !== null ? saved === '1' : window.matchMedia('(prefers-color-scheme: dark)').matches;
|
| 2172 |
-
applyDark(prefersDark);
|
| 2173 |
-
}} catch(e) {{}}
|
| 2174 |
}})();
|
| 2175 |
|
| 2176 |
-
// Image zoom
|
| 2177 |
-
|
| 2178 |
document.getElementById('img-modal-src').src = src;
|
| 2179 |
document.getElementById('img-modal').classList.add('open');
|
| 2180 |
-
}}
|
| 2181 |
-
|
| 2182 |
document.getElementById('img-modal').classList.remove('open');
|
| 2183 |
document.getElementById('img-modal-src').src = '';
|
| 2184 |
-
}}
|
| 2185 |
-
document.addEventListener('keydown', function(e) {{ if (e.key === 'Escape') closeModal(); }});
|
| 2186 |
-
|
| 2187 |
-
// Lucide icons
|
| 2188 |
-
lucide.createIcons();
|
| 2189 |
|
| 2190 |
// Live search + filter chips
|
| 2191 |
-
var activeChips = {{}};
|
| 2192 |
|
| 2193 |
-
|
| 2194 |
var group = el.dataset.group;
|
| 2195 |
var val = el.dataset.val;
|
| 2196 |
if (!activeChips[group]) activeChips[group] = new Set();
|
|
@@ -2198,7 +2292,6 @@ async def latest_questions_page(courseId: int, chapterId: Optional[int] = None,
|
|
| 2198 |
activeChips[group].delete(val);
|
| 2199 |
el.classList.remove('active');
|
| 2200 |
}} else {{
|
| 2201 |
-
// For answer + dup groups only allow one active at a time
|
| 2202 |
if (group === 'answer' || group === 'dup') {{
|
| 2203 |
document.querySelectorAll('.chip[data-group="' + group + '"]').forEach(function(c) {{
|
| 2204 |
c.classList.remove('active');
|
|
@@ -2208,45 +2301,35 @@ async def latest_questions_page(courseId: int, chapterId: Optional[int] = None,
|
|
| 2208 |
activeChips[group].add(val);
|
| 2209 |
el.classList.add('active');
|
| 2210 |
}}
|
| 2211 |
-
applyFilters();
|
| 2212 |
-
}}
|
| 2213 |
|
| 2214 |
-
|
| 2215 |
var search = (document.getElementById('q-search').value || '').toLowerCase().trim();
|
| 2216 |
var cards = document.querySelectorAll('.q-card');
|
| 2217 |
var visible = 0;
|
| 2218 |
-
|
| 2219 |
cards.forEach(function(card) {{
|
| 2220 |
-
// Search: match id or text
|
| 2221 |
var idMatch = !search || card.dataset.id.includes(search);
|
| 2222 |
var textMatch = !search || card.dataset.text.includes(search);
|
| 2223 |
if (!idMatch && !textMatch) {{ card.style.display = 'none'; return; }}
|
| 2224 |
-
|
| 2225 |
-
// Chip filters — each group is OR within, AND across groups
|
| 2226 |
for (var group in activeChips) {{
|
| 2227 |
if (!activeChips[group].size) continue;
|
| 2228 |
-
|
| 2229 |
-
if (!activeChips[group].has(field)) {{ card.style.display = 'none'; return; }}
|
| 2230 |
}}
|
| 2231 |
-
|
| 2232 |
card.style.display = '';
|
| 2233 |
visible++;
|
| 2234 |
}});
|
| 2235 |
-
|
| 2236 |
-
// Hide batch blocks that have no visible cards
|
| 2237 |
document.querySelectorAll('.batch-block').forEach(function(block) {{
|
| 2238 |
var anyVisible = Array.from(block.querySelectorAll('.q-card')).some(function(c) {{
|
| 2239 |
return c.style.display !== 'none';
|
| 2240 |
}});
|
| 2241 |
block.style.display = anyVisible ? '' : 'none';
|
| 2242 |
}});
|
| 2243 |
-
|
| 2244 |
var fc = document.getElementById('filter-count');
|
| 2245 |
if (fc) fc.textContent = (search || Object.values(activeChips).some(function(s){{return s.size;}}))
|
| 2246 |
? visible + ' match' + (visible !== 1 ? 'es' : '') : '';
|
| 2247 |
-
}}
|
| 2248 |
-
</script>
|
| 2249 |
-
</body></html>"""
|
| 2250 |
|
| 2251 |
|
| 2252 |
if __name__ == "__main__":
|
|
|
|
| 1668 |
|
| 1669 |
|
| 1670 |
@app.get("/latest-questions", response_class=HTMLResponse)
|
| 1671 |
+
async def latest_questions_shell(courseId: int = 30, chapterId: Optional[int] = None, limit: int = 100):
|
| 1672 |
+
"""Shell page — loads instantly, then fetches content via AJAX."""
|
| 1673 |
+
chapter_param = f"&chapterId={chapterId}" if chapterId is not None else ""
|
| 1674 |
+
data_url = f"/latest-questions-data?courseId={courseId}{chapter_param}&limit={limit}"
|
| 1675 |
+
return f"""<!doctype html>
|
| 1676 |
+
<html lang="en"><head>
|
| 1677 |
+
<meta charset="utf-8">
|
| 1678 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 1679 |
+
<title>Latest Questions — course {courseId}</title>
|
| 1680 |
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.css">
|
| 1681 |
+
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/katex.min.js"></script>
|
| 1682 |
+
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.11/dist/contrib/auto-render.min.js"></script>
|
| 1683 |
+
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.min.js"></script>
|
| 1684 |
+
<style>
|
| 1685 |
+
*, *::before, *::after {{ box-sizing: border-box; margin: 0; padding: 0; }}
|
| 1686 |
+
body {{ font-family: system-ui, sans-serif; background: #f8fafc; color: #1e293b; line-height: 1.5; }}
|
| 1687 |
+
body.dark {{ background: #0f172a; color: #e2e8f0; }}
|
| 1688 |
+
.container {{ max-width: 1200px; margin: 0 auto; padding: 28px 16px 60px; }}
|
| 1689 |
+
h1 {{ font-size: 1.4rem; font-weight: 700; }}
|
| 1690 |
+
.btn {{ padding: 8px 16px; border: none; border-radius: 6px; cursor: pointer; font-size: 0.85rem; font-weight: 600; font-family: inherit; transition: background 0.12s; display: inline-flex; align-items: center; gap: 6px; }}
|
| 1691 |
+
.btn-secondary {{ background: #f1f5f9; color: #475569; border: 1px solid #e2e8f0; }}
|
| 1692 |
+
.btn-secondary:hover {{ background: #e2e8f0; }}
|
| 1693 |
+
.btn-sm {{ padding: 5px 12px; font-size: 0.78rem; }}
|
| 1694 |
+
.dm-toggle {{ display: inline-flex; align-items: center; justify-content: center; width: 32px; height: 32px; border-radius: 6px; border: 1px solid #e2e8f0; background: #f1f5f9; color: #475569; cursor: pointer; transition: background 0.15s, border-color 0.15s, color 0.15s; flex-shrink: 0; }}
|
| 1695 |
+
.dm-toggle:hover {{ background: #e2e8f0; color: #1e293b; }}
|
| 1696 |
+
body.dark .btn-secondary {{ background: #1e293b; color: #94a3b8; border-color: #334155; }}
|
| 1697 |
+
body.dark .btn-secondary:hover {{ background: #334155; color: #e2e8f0; }}
|
| 1698 |
+
body.dark .dm-toggle {{ background: #1e293b; border-color: #334155; color: #94a3b8; }}
|
| 1699 |
+
body.dark .dm-toggle:hover {{ background: #334155; color: #e2e8f0; }}
|
| 1700 |
+
|
| 1701 |
+
/* ── Loading scene ── */
|
| 1702 |
+
#loading-scene {{
|
| 1703 |
+
display: flex; flex-direction: column; align-items: center;
|
| 1704 |
+
justify-content: center; padding: 80px 20px 100px; gap: 28px;
|
| 1705 |
+
}}
|
| 1706 |
+
.loader-books {{
|
| 1707 |
+
display: flex; gap: 10px; align-items: flex-end; height: 60px;
|
| 1708 |
+
}}
|
| 1709 |
+
.book {{
|
| 1710 |
+
width: 18px; border-radius: 3px 3px 0 0;
|
| 1711 |
+
animation: bookBounce 1.1s ease-in-out infinite;
|
| 1712 |
+
transform-origin: bottom center;
|
| 1713 |
+
}}
|
| 1714 |
+
.book:nth-child(1) {{ height: 46px; background: #2563eb; animation-delay: 0s; }}
|
| 1715 |
+
.book:nth-child(2) {{ height: 54px; background: #7c3aed; animation-delay: 0.15s; }}
|
| 1716 |
+
.book:nth-child(3) {{ height: 38px; background: #059669; animation-delay: 0.30s; }}
|
| 1717 |
+
.book:nth-child(4) {{ height: 58px; background: #d97706; animation-delay: 0.45s; }}
|
| 1718 |
+
.book:nth-child(5) {{ height: 42px; background: #dc2626; animation-delay: 0.60s; }}
|
| 1719 |
+
@keyframes bookBounce {{
|
| 1720 |
+
0%, 100% {{ transform: scaleY(1); opacity: 1; }}
|
| 1721 |
+
50% {{ transform: scaleY(0.55); opacity: 0.6; }}
|
| 1722 |
+
}}
|
| 1723 |
+
.loader-dots {{
|
| 1724 |
+
display: flex; gap: 7px;
|
| 1725 |
+
}}
|
| 1726 |
+
.loader-dots span {{
|
| 1727 |
+
width: 8px; height: 8px; border-radius: 50%; background: #94a3b8;
|
| 1728 |
+
animation: dotPulse 1.4s ease-in-out infinite;
|
| 1729 |
+
}}
|
| 1730 |
+
.loader-dots span:nth-child(2) {{ animation-delay: 0.2s; }}
|
| 1731 |
+
.loader-dots span:nth-child(3) {{ animation-delay: 0.4s; }}
|
| 1732 |
+
@keyframes dotPulse {{
|
| 1733 |
+
0%, 80%, 100% {{ transform: scale(0.7); opacity: 0.5; }}
|
| 1734 |
+
40% {{ transform: scale(1); opacity: 1; background: #2563eb; }}
|
| 1735 |
+
}}
|
| 1736 |
+
.loader-msg {{
|
| 1737 |
+
font-size: 0.9rem; color: #64748b; text-align: center; max-width: 300px; line-height: 1.6;
|
| 1738 |
+
}}
|
| 1739 |
+
.loader-msg strong {{ color: #1e293b; display: block; font-size: 1rem; margin-bottom: 4px; }}
|
| 1740 |
+
body.dark .loader-msg {{ color: #475569; }}
|
| 1741 |
+
body.dark .loader-msg strong {{ color: #e2e8f0; }}
|
| 1742 |
+
#error-scene {{
|
| 1743 |
+
display: none; text-align: center; padding: 60px 20px;
|
| 1744 |
+
color: #dc2626; font-size: 0.9rem;
|
| 1745 |
+
}}
|
| 1746 |
+
body.dark #error-scene {{ color: #f87171; }}
|
| 1747 |
+
</style>
|
| 1748 |
+
</head>
|
| 1749 |
+
<body>
|
| 1750 |
+
<div class="container">
|
| 1751 |
+
<div style="display:flex;align-items:flex-start;justify-content:space-between;gap:12px;margin-bottom:20px;">
|
| 1752 |
+
<h1>Latest Saved Questions</h1>
|
| 1753 |
+
<div style="display:flex;gap:8px;align-items:center;">
|
| 1754 |
+
<a href="/" class="btn btn-secondary btn-sm" style="text-decoration:none;">
|
| 1755 |
+
<i data-lucide="arrow-left" style="width:13px;height:13px;"></i> Back
|
| 1756 |
+
</a>
|
| 1757 |
+
<button class="dm-toggle" onclick="toggleDark()" title="Toggle dark mode" aria-label="Toggle dark mode">
|
| 1758 |
+
<svg id="dm-icon-moon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
|
| 1759 |
+
<svg id="dm-icon-sun" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:none;"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>
|
| 1760 |
+
</button>
|
| 1761 |
+
</div>
|
| 1762 |
+
</div>
|
| 1763 |
+
|
| 1764 |
+
<!-- Loading animation -->
|
| 1765 |
+
<div id="loading-scene">
|
| 1766 |
+
<div class="loader-books">
|
| 1767 |
+
<div class="book"></div><div class="book"></div><div class="book"></div>
|
| 1768 |
+
<div class="book"></div><div class="book"></div>
|
| 1769 |
+
</div>
|
| 1770 |
+
<div class="loader-dots">
|
| 1771 |
+
<span></span><span></span><span></span>
|
| 1772 |
+
</div>
|
| 1773 |
+
<div class="loader-msg">
|
| 1774 |
+
<strong>Fetching questions...</strong>
|
| 1775 |
+
Pulling from the question bank and checking for duplicates.
|
| 1776 |
+
</div>
|
| 1777 |
+
</div>
|
| 1778 |
+
<div id="error-scene"></div>
|
| 1779 |
+
|
| 1780 |
+
<!-- Content injected here -->
|
| 1781 |
+
<div id="content"></div>
|
| 1782 |
+
</div>
|
| 1783 |
+
|
| 1784 |
+
<script>
|
| 1785 |
+
// Dark mode
|
| 1786 |
+
function applyDark(dark) {{
|
| 1787 |
+
document.body.classList.toggle('dark', dark);
|
| 1788 |
+
document.getElementById('dm-icon-moon').style.display = dark ? 'none' : '';
|
| 1789 |
+
document.getElementById('dm-icon-sun').style.display = dark ? '' : 'none';
|
| 1790 |
+
}}
|
| 1791 |
+
function toggleDark() {{
|
| 1792 |
+
var isDark = document.body.classList.contains('dark');
|
| 1793 |
+
applyDark(!isDark);
|
| 1794 |
+
try {{ localStorage.setItem('dm', !isDark ? '1' : '0'); }} catch(e) {{}}
|
| 1795 |
+
}}
|
| 1796 |
+
(function() {{
|
| 1797 |
+
try {{
|
| 1798 |
+
var saved = localStorage.getItem('dm');
|
| 1799 |
+
applyDark(saved !== null ? saved === '1' : window.matchMedia('(prefers-color-scheme: dark)').matches);
|
| 1800 |
+
}} catch(e) {{}}
|
| 1801 |
+
}})();
|
| 1802 |
+
|
| 1803 |
+
lucide.createIcons();
|
| 1804 |
+
|
| 1805 |
+
// Fetch data
|
| 1806 |
+
fetch("{data_url}")
|
| 1807 |
+
.then(function(r) {{
|
| 1808 |
+
if (!r.ok) throw new Error('HTTP ' + r.status);
|
| 1809 |
+
return r.text();
|
| 1810 |
+
}})
|
| 1811 |
+
.then(function(fragHtml) {{
|
| 1812 |
+
document.getElementById('loading-scene').style.display = 'none';
|
| 1813 |
+
var el = document.getElementById('content');
|
| 1814 |
+
// Use createContextualFragment so <script> tags execute
|
| 1815 |
+
var frag = document.createRange().createContextualFragment(fragHtml);
|
| 1816 |
+
el.appendChild(frag);
|
| 1817 |
+
// Re-run lucide on injected icons
|
| 1818 |
+
lucide.createIcons();
|
| 1819 |
+
// KaTeX (may already be loaded; retry if deferred scripts aren't done yet)
|
| 1820 |
+
function tryRenderMath() {{
|
| 1821 |
+
if (window.renderMathInElement) {{
|
| 1822 |
+
renderMathInElement(el, {{ delimiters: [
|
| 1823 |
+
{{left: '$$', right: '$$', display: true}},
|
| 1824 |
+
{{left: '$', right: '$', display: false}}
|
| 1825 |
+
]}});
|
| 1826 |
+
}} else {{
|
| 1827 |
+
setTimeout(tryRenderMath, 200);
|
| 1828 |
+
}}
|
| 1829 |
+
}}
|
| 1830 |
+
tryRenderMath();
|
| 1831 |
+
}})
|
| 1832 |
+
.catch(function(err) {{
|
| 1833 |
+
document.getElementById('loading-scene').style.display = 'none';
|
| 1834 |
+
var es = document.getElementById('error-scene');
|
| 1835 |
+
es.style.display = 'block';
|
| 1836 |
+
es.innerHTML = '<strong style="font-size:1rem;display:block;margin-bottom:8px;">Failed to load questions</strong>' + err.message
|
| 1837 |
+
+ '<br><br><a href="" style="color:#2563eb;">Retry</a>';
|
| 1838 |
+
}});
|
| 1839 |
+
</script>
|
| 1840 |
+
</body></html>"""
|
| 1841 |
+
|
| 1842 |
+
|
| 1843 |
+
@app.get("/latest-questions-data", response_class=HTMLResponse)
|
| 1844 |
+
async def latest_questions_data(courseId: int, chapterId: Optional[int] = None, limit: int = 100):
|
| 1845 |
+
"""Data fragment — fetched by the shell page via AJAX."""
|
| 1846 |
from collections import defaultdict
|
| 1847 |
from datetime import datetime as _dt, timezone as _tz
|
| 1848 |
|
|
|
|
| 2100 |
f'<span class="chip-sep"></span>'
|
| 2101 |
) if values else ""
|
| 2102 |
|
| 2103 |
+
return f"""<style>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2104 |
.subtitle {{ color: #64748b; margin: 3px 0 20px; font-size: 0.85rem; }}
|
| 2105 |
h2 {{ font-size: 0.8rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: #64748b; margin: 24px 0 10px; }}
|
| 2106 |
+
.btn-primary {{ background: #2563eb; color: white; border: none; }}
|
|
|
|
|
|
|
| 2107 |
.btn-primary:hover {{ background: #1d4ed8; }}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2108 |
.card {{ background: white; border: 1px solid #e2e8f0; border-radius: 8px; padding: 16px 20px; margin-bottom: 10px; }}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2109 |
.filter-row {{ display: flex; gap: 10px; align-items: flex-end; flex-wrap: wrap; }}
|
| 2110 |
.filter-row label {{ display: flex; flex-direction: column; gap: 3px; font-size: 0.72rem; font-weight: 600; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em; }}
|
| 2111 |
.filter-row input[type=number] {{ width: 100px; padding: 6px 8px; border: 1px solid #cbd5e1; border-radius: 6px; font-size: 0.85rem; font-family: inherit; -moz-appearance: textfield; appearance: textfield; }}
|
| 2112 |
.filter-row input[type=number]::-webkit-outer-spin-button,
|
| 2113 |
.filter-row input[type=number]::-webkit-inner-spin-button {{ -webkit-appearance: none; margin: 0; }}
|
| 2114 |
.filter-row input:focus {{ outline: none; border-color: #2563eb; }}
|
|
|
|
| 2115 |
.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; }}
|
| 2116 |
.search-bar:focus {{ outline: none; border-color: #2563eb; }}
|
| 2117 |
.chip-row {{ display: flex; gap: 6px; flex-wrap: wrap; margin-top: 10px; align-items: center; }}
|
|
|
|
| 2121 |
.chip.active {{ background: #2563eb; color: #fff; border-color: #2563eb; }}
|
| 2122 |
.chip-sep {{ width: 1px; height: 18px; background: #e2e8f0; margin: 0 2px; }}
|
| 2123 |
.filter-count {{ font-size: 0.72rem; color: #64748b; margin-left: auto; }}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2124 |
.summary-strip {{ display: flex; flex-wrap: wrap; gap: 20px; }}
|
| 2125 |
.summary-strip .stat {{ display: flex; flex-direction: column; }}
|
| 2126 |
.summary-strip .stat-val {{ font-size: 1.5rem; font-weight: 700; line-height: 1.1; }}
|
| 2127 |
.summary-strip .stat-lbl {{ font-size: 0.72rem; color: #64748b; text-transform: uppercase; letter-spacing: 0.05em; }}
|
|
|
|
| 2128 |
.dup-card {{ border-left: 3px solid #dc2626; }}
|
| 2129 |
.dup-count {{ display: inline-flex; align-items: center; gap: 5px; font-size: 0.72rem; font-weight: 700; color: #dc2626; text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 8px; }}
|
| 2130 |
+
.qtext {{ font-size: 0.875rem; line-height: 1.7; margin-bottom: 10px; word-break: break-word; overflow-wrap: break-word; }}
|
| 2131 |
.meta-list {{ list-style: none; padding: 0; display: flex; flex-direction: column; gap: 4px; }}
|
| 2132 |
.meta-list li {{ font-size: 0.75rem; color: #64748b; background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 5px; padding: 4px 8px; font-family: monospace; }}
|
|
|
|
| 2133 |
.badge {{ padding: 2px 7px; border-radius: 9999px; font-size: 0.65rem; font-weight: 600; white-space: nowrap; }}
|
| 2134 |
.badge-blue {{ background: #dbeafe; color: #1d4ed8; }}
|
| 2135 |
.badge-green {{ background: #dcfce7; color: #166534; }}
|
| 2136 |
.badge-gray {{ background: #f1f5f9; color: #64748b; }}
|
| 2137 |
.badge-red {{ background: #fee2e2; color: #dc2626; }}
|
|
|
|
| 2138 |
.batch-block {{ margin-bottom: 24px; }}
|
| 2139 |
.batch-header {{ display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }}
|
| 2140 |
.batch-meta {{ display: flex; align-items: center; gap: 7px; font-size: 0.78rem; flex-wrap: wrap; }}
|
| 2141 |
.batch-time {{ font-weight: 600; color: #1e293b; }}
|
| 2142 |
.batch-sep {{ color: #cbd5e1; }}
|
|
|
|
|
|
|
|
|
|
| 2143 |
.cards-grid {{ display: flex; flex-direction: column; gap: 12px; }}
|
|
|
|
| 2144 |
.q-card {{ background: white; border: 1px solid #e2e8f0; border-radius: 10px; overflow: hidden; }}
|
| 2145 |
.dup-card-q {{ border-left: 3px solid #f59e0b; }}
|
|
|
|
| 2146 |
.card-topbar {{ display: flex; align-items: center; gap: 6px; flex-wrap: wrap; padding: 10px 14px; border-bottom: 1px solid #f1f5f9; background: #f8fafc; }}
|
| 2147 |
.card-id {{ font-family: monospace; }}
|
| 2148 |
.card-ts {{ margin-left: auto; display: inline-flex; align-items: center; gap: 4px; font-size: 0.7rem; color: #94a3b8; white-space: nowrap; }}
|
| 2149 |
.dup-marker-badge {{ display: inline-flex; align-items: center; gap: 3px; font-size: 0.65rem; font-weight: 700; color: #d97706; background: #fef3c7; border: 1px solid #fde68a; border-radius: 9999px; padding: 1px 7px; }}
|
|
|
|
| 2150 |
.card-body-split {{ display: grid; grid-template-columns: 1fr auto; gap: 16px; padding: 16px 14px; align-items: start; }}
|
| 2151 |
.card-body-single {{ padding: 16px 14px; }}
|
| 2152 |
+
.card-text {{ font-size: 0.9rem; line-height: 1.75; color: #1e293b; word-break: break-word; overflow-wrap: break-word; }}
|
|
|
|
| 2153 |
.card-img-wrap {{ flex-shrink: 0; }}
|
| 2154 |
.card-img-wrap img {{ max-width: 220px; max-height: 180px; border: 1px solid #e2e8f0; border-radius: 7px; display: block; cursor: zoom-in; object-fit: contain; }}
|
|
|
|
| 2155 |
.card-section {{ padding: 10px 14px; border-top: 1px solid #f1f5f9; }}
|
| 2156 |
.section-label {{ display: block; font-size: 0.65rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; color: #94a3b8; margin-bottom: 4px; }}
|
| 2157 |
+
.card-ans {{ font-size: 0.82rem; line-height: 1.65; color: #1e293b; white-space: pre-wrap; word-break: break-word; overflow-wrap: break-word; }}
|
| 2158 |
+
.card-hint {{ font-size: 0.82rem; line-height: 1.65; color: #64748b; font-style: italic; white-space: pre-wrap; word-break: break-word; overflow-wrap: break-word; }}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2159 |
.img-modal {{ position: fixed; inset: 0; background: rgba(15,23,42,0.8); display: none; align-items: center; justify-content: center; z-index: 300; padding: 18px; }}
|
| 2160 |
.img-modal.open {{ display: flex; }}
|
| 2161 |
.img-modal img {{ max-width: min(1100px,96vw); max-height: 90vh; border-radius: 8px; box-shadow: 0 16px 40px rgba(15,23,42,0.45); background: white; object-fit: contain; }}
|
| 2162 |
.img-modal-close {{ position: absolute; top: 14px; right: 14px; border: 1px solid #94a3b8; background: #0f172a; color: white; border-radius: 6px; font-size: 0.78rem; padding: 6px 10px; cursor: pointer; }}
|
| 2163 |
.muted {{ color: #94a3b8; font-size: 0.78rem; }}
|
| 2164 |
/* Dark mode */
|
|
|
|
| 2165 |
body.dark .card {{ background: #1e293b; border-color: #334155; }}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2166 |
body.dark .subtitle {{ color: #64748b; }}
|
| 2167 |
body.dark h2 {{ color: #475569; }}
|
| 2168 |
body.dark .filter-row label {{ color: #64748b; }}
|
| 2169 |
body.dark .filter-row input[type=number] {{ background: #0f172a; border-color: #334155; color: #e2e8f0; }}
|
| 2170 |
body.dark .filter-row input:focus {{ border-color: #2563eb; }}
|
| 2171 |
+
body.dark .search-bar {{ background-color: #0f172a; border-color: #334155; color: #e2e8f0; }}
|
| 2172 |
+
body.dark .search-bar:focus {{ border-color: #2563eb; }}
|
| 2173 |
+
body.dark .chip {{ background: #1e293b; border-color: #334155; color: #94a3b8; }}
|
| 2174 |
+
body.dark .chip:hover {{ background: #334155; color: #e2e8f0; }}
|
| 2175 |
+
body.dark .chip.active {{ background: #2563eb; color: #fff; border-color: #2563eb; }}
|
| 2176 |
+
body.dark .chip-sep {{ background: #334155; }}
|
| 2177 |
+
body.dark .filter-count {{ color: #475569; }}
|
| 2178 |
body.dark .summary-strip .stat-lbl {{ color: #475569; }}
|
| 2179 |
body.dark .dup-card {{ border-left-color: #f87171; }}
|
| 2180 |
body.dark .dup-count {{ color: #f87171; }}
|
|
|
|
| 2184 |
body.dark .badge-gray {{ background: #334155; color: #94a3b8; }}
|
| 2185 |
body.dark .badge-red {{ background: #450a0a; color: #f87171; }}
|
| 2186 |
body.dark .muted {{ color: #475569; }}
|
| 2187 |
+
body.dark .batch-time {{ color: #e2e8f0; }}
|
| 2188 |
+
body.dark .batch-sep {{ color: #334155; }}
|
| 2189 |
+
body.dark .q-card {{ background: #1e293b; border-color: #334155; }}
|
| 2190 |
+
body.dark .dup-card-q {{ border-left-color: #f59e0b; }}
|
| 2191 |
+
body.dark .card-topbar {{ background: #0f172a; border-bottom-color: #334155; }}
|
| 2192 |
+
body.dark .card-text {{ color: #e2e8f0; }}
|
| 2193 |
+
body.dark .card-section {{ border-top-color: #334155; }}
|
| 2194 |
+
body.dark .card-ans {{ color: #e2e8f0; }}
|
| 2195 |
+
body.dark .card-hint {{ color: #94a3b8; }}
|
| 2196 |
+
body.dark .card-img-wrap img {{ border-color: #334155; }}
|
| 2197 |
</style>
|
|
|
|
|
|
|
|
|
|
| 2198 |
|
| 2199 |
+
<p class="subtitle">Course {courseId} — {chapter_label} — {len(questions)} of {total} questions — generated {generated}</p>
|
| 2200 |
+
|
| 2201 |
+
<!-- Filter card -->
|
| 2202 |
+
<div class="card" style="margin-bottom:16px;">
|
| 2203 |
+
<form method="get" action="/latest-questions">
|
| 2204 |
+
<div class="filter-row">
|
| 2205 |
+
<label>Course ID
|
| 2206 |
+
<input type="number" name="courseId" value="{courseId}" required>
|
| 2207 |
+
</label>
|
| 2208 |
+
<label>Chapter ID (optional)
|
| 2209 |
+
<input type="number" name="chapterId" value="{chapterId if chapterId is not None else ''}">
|
| 2210 |
+
</label>
|
| 2211 |
+
<label>Limit
|
| 2212 |
+
<input type="number" name="limit" value="{limit}" min="1" max="500">
|
| 2213 |
+
</label>
|
| 2214 |
+
<button type="submit" class="btn btn-primary btn-sm" style="margin-bottom:1px;">
|
| 2215 |
+
<i data-lucide="refresh-cw" style="width:13px;height:13px;"></i>
|
| 2216 |
+
Refresh
|
| 2217 |
</button>
|
| 2218 |
</div>
|
| 2219 |
+
</form>
|
| 2220 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2221 |
|
| 2222 |
+
<!-- Summary -->
|
| 2223 |
+
<div class="card" style="margin-bottom:16px;">
|
| 2224 |
+
<div class="summary-strip">
|
| 2225 |
+
<div class="stat"><span class="stat-val">{len(questions)}</span><span class="stat-lbl">Showing</span></div>
|
| 2226 |
+
<div class="stat"><span class="stat-val">{total}</span><span class="stat-lbl">Total</span></div>
|
| 2227 |
+
<div class="stat"><span class="stat-val">{len(dups)}</span><span class="stat-lbl">Dup groups</span></div>
|
| 2228 |
+
<div class="stat"><span class="stat-val">{sum(len(v) for v in dups.values())}</span><span class="stat-lbl">Dup rows</span></div>
|
|
|
|
| 2229 |
</div>
|
| 2230 |
+
</div>
|
| 2231 |
|
| 2232 |
+
{empty}
|
| 2233 |
+
|
| 2234 |
+
<!-- Live search + filter chips -->
|
| 2235 |
+
<div class="card" style="margin-bottom:16px;">
|
| 2236 |
+
<input class="search-bar" id="q-search" type="text" placeholder="Search by ID or question text…" oninput="applyFilters()">
|
| 2237 |
+
<div class="chip-row" id="chip-area">
|
| 2238 |
+
{chip_row("type", types_vals, "Type")}
|
| 2239 |
+
{chip_row("difficulty", diff_vals, "Difficulty")}
|
| 2240 |
+
{chip_row("bloom", bloom_vals, "Bloom")}
|
| 2241 |
+
{chip_row("status", status_vals, "Status")}
|
| 2242 |
+
<span class="chip" data-group="answer" data-val="yes" onclick="toggleChip(this)">Has answer</span>
|
| 2243 |
+
<span class="chip" data-group="answer" data-val="no" onclick="toggleChip(this)">No answer</span>
|
| 2244 |
+
<span class="chip-sep"></span>
|
| 2245 |
+
<span class="chip" data-group="dup" data-val="yes" onclick="toggleChip(this)">Duplicates only</span>
|
| 2246 |
+
<span class="filter-count" id="filter-count"></span>
|
|
|
|
| 2247 |
</div>
|
| 2248 |
+
</div>
|
| 2249 |
|
| 2250 |
+
<!-- Duplicates -->
|
| 2251 |
+
<h2><i data-lucide="copy" style="width:12px;height:12px;display:inline;vertical-align:middle;margin-right:4px;"></i>Duplicates (exact text)</h2>
|
| 2252 |
+
{dup_html}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2253 |
|
| 2254 |
+
<!-- Batch groups -->
|
| 2255 |
+
<h2><i data-lucide="layers" style="width:12px;height:12px;display:inline;vertical-align:middle;margin-right:4px;"></i>Save batches (grouped by minute)</h2>
|
| 2256 |
+
{batches_html}
|
| 2257 |
|
| 2258 |
+
<!-- Image zoom modal (appended to body by script below) -->
|
| 2259 |
+
<template id="modal-tpl">
|
| 2260 |
+
<div class="img-modal" id="img-modal" onclick="closeModal()">
|
| 2261 |
+
<button class="img-modal-close" onclick="closeModal()">Close</button>
|
| 2262 |
+
<img id="img-modal-src" src="" alt="figure">
|
| 2263 |
+
</div>
|
| 2264 |
+
</template>
|
| 2265 |
|
| 2266 |
<script>
|
| 2267 |
+
// Mount modal onto body
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2268 |
(function() {{
|
| 2269 |
+
var tpl = document.getElementById('modal-tpl');
|
| 2270 |
+
if (tpl) document.body.appendChild(tpl.content.cloneNode(true));
|
|
|
|
|
|
|
|
|
|
| 2271 |
}})();
|
| 2272 |
|
| 2273 |
+
// Image zoom (global so inline onclick works)
|
| 2274 |
+
window.openModal = function(src) {{
|
| 2275 |
document.getElementById('img-modal-src').src = src;
|
| 2276 |
document.getElementById('img-modal').classList.add('open');
|
| 2277 |
+
}};
|
| 2278 |
+
window.closeModal = function() {{
|
| 2279 |
document.getElementById('img-modal').classList.remove('open');
|
| 2280 |
document.getElementById('img-modal-src').src = '';
|
| 2281 |
+
}};
|
| 2282 |
+
document.addEventListener('keydown', function(e) {{ if (e.key === 'Escape') window.closeModal(); }});
|
|
|
|
|
|
|
|
|
|
| 2283 |
|
| 2284 |
// Live search + filter chips
|
| 2285 |
+
var activeChips = {{}};
|
| 2286 |
|
| 2287 |
+
window.toggleChip = function(el) {{
|
| 2288 |
var group = el.dataset.group;
|
| 2289 |
var val = el.dataset.val;
|
| 2290 |
if (!activeChips[group]) activeChips[group] = new Set();
|
|
|
|
| 2292 |
activeChips[group].delete(val);
|
| 2293 |
el.classList.remove('active');
|
| 2294 |
}} else {{
|
|
|
|
| 2295 |
if (group === 'answer' || group === 'dup') {{
|
| 2296 |
document.querySelectorAll('.chip[data-group="' + group + '"]').forEach(function(c) {{
|
| 2297 |
c.classList.remove('active');
|
|
|
|
| 2301 |
activeChips[group].add(val);
|
| 2302 |
el.classList.add('active');
|
| 2303 |
}}
|
| 2304 |
+
window.applyFilters();
|
| 2305 |
+
}};
|
| 2306 |
|
| 2307 |
+
window.applyFilters = function() {{
|
| 2308 |
var search = (document.getElementById('q-search').value || '').toLowerCase().trim();
|
| 2309 |
var cards = document.querySelectorAll('.q-card');
|
| 2310 |
var visible = 0;
|
|
|
|
| 2311 |
cards.forEach(function(card) {{
|
|
|
|
| 2312 |
var idMatch = !search || card.dataset.id.includes(search);
|
| 2313 |
var textMatch = !search || card.dataset.text.includes(search);
|
| 2314 |
if (!idMatch && !textMatch) {{ card.style.display = 'none'; return; }}
|
|
|
|
|
|
|
| 2315 |
for (var group in activeChips) {{
|
| 2316 |
if (!activeChips[group].size) continue;
|
| 2317 |
+
if (!activeChips[group].has(card.dataset[group])) {{ card.style.display = 'none'; return; }}
|
|
|
|
| 2318 |
}}
|
|
|
|
| 2319 |
card.style.display = '';
|
| 2320 |
visible++;
|
| 2321 |
}});
|
|
|
|
|
|
|
| 2322 |
document.querySelectorAll('.batch-block').forEach(function(block) {{
|
| 2323 |
var anyVisible = Array.from(block.querySelectorAll('.q-card')).some(function(c) {{
|
| 2324 |
return c.style.display !== 'none';
|
| 2325 |
}});
|
| 2326 |
block.style.display = anyVisible ? '' : 'none';
|
| 2327 |
}});
|
|
|
|
| 2328 |
var fc = document.getElementById('filter-count');
|
| 2329 |
if (fc) fc.textContent = (search || Object.values(activeChips).some(function(s){{return s.size;}}))
|
| 2330 |
? visible + ' match' + (visible !== 1 ? 'es' : '') : '';
|
| 2331 |
+
}};
|
| 2332 |
+
</script>"""
|
|
|
|
| 2333 |
|
| 2334 |
|
| 2335 |
if __name__ == "__main__":
|