fix: AMBOSS-style filter side box on question bank, create-page layout fixes
Bank filters (state, difficulty, categories with educator controls, tags) live in a left side box; main column keeps search and list. Create-page settings/actions grouped into one column with full-contrast primary button. 97 frontend tests pass.
This commit is contained in:
parent
236547e646
commit
6fbfedd8a9
4 changed files with 81 additions and 41 deletions
|
|
@ -46,3 +46,9 @@ details[open] > .custom-test-branch .custom-test-chevron { transform: rotate(45d
|
|||
.custom-test-adaptive input:checked + .custom-test-switch { background: var(--primary); }
|
||||
.custom-test-adaptive input:checked + .custom-test-switch span { transform: translateX(17px); }
|
||||
.custom-test-branch { display: flex; align-items: center; }
|
||||
.custom-test-main { gap: 0; }
|
||||
.custom-test-settings > p, .custom-test-settings > .custom-test-share { margin: 0; }
|
||||
.custom-test-actions { display: flex; gap: 10px; align-items: center; margin-top: 12px; }
|
||||
.custom-test-actions .btn-primary { flex: 1; background: var(--primary); color: var(--primary-fg); font-weight: 600; padding: 10px 16px; }
|
||||
.custom-test-main > p { margin: 8px 0 0; }
|
||||
.custom-test-main > .custom-test-share { margin-top: 8px; }
|
||||
|
|
|
|||
|
|
@ -150,9 +150,11 @@ export default function CustomQuizPage() {
|
|||
{ready && available === 0 && <p>No questions match these filters.</p>}
|
||||
{ready && !validCount && available > 0 && <p>Choose 1–{Math.min(200, available)} questions.</p>}
|
||||
{countError && <p role="alert">{countError}</p>}
|
||||
<button type="button" className="btn btn-secondary" disabled={submitting} onClick={() => setRefresh(v => v + 1)}>Refresh count</button>
|
||||
<div className="custom-test-actions">
|
||||
<button type="button" className="btn btn-secondary" disabled={submitting} onClick={() => setRefresh(v => v + 1)}>Refresh count</button>
|
||||
<button className="btn btn-primary" type="submit" disabled={submitting || !ready || !validCount || !title.trim()}>{submitting ? 'Creating…' : 'Create Test'}</button>
|
||||
</div>
|
||||
{error && <p role="alert">{error}</p>}
|
||||
<button className="btn btn-primary" type="submit" disabled={submitting || !ready || !validCount || !title.trim()}>{submitting ? 'Creating…' : 'Create Test'}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,3 +5,19 @@
|
|||
.category-editor-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.45); z-index: 1100; display: flex; align-items: center; justify-content: center; padding: 16px; }
|
||||
.category-editor-card { background: var(--card-bg); border-radius: 12px; padding: 18px; width: 100%; max-width: 420px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); }
|
||||
.category-editor-card select { width: 100%; padding: 7px 12px; border: 1px solid var(--border); border-radius: 8px; font-size: .875rem; background: var(--input-bg); color: var(--text); }
|
||||
.bank-layout { display: grid; grid-template-columns: 280px 1fr; gap: 16px; align-items: start; }
|
||||
.bank-filters { padding: 14px; }
|
||||
.bank-filters h2 { margin: 0 0 8px; font-size: 1rem; }
|
||||
.bank-filters h3 { margin: 14px 0 6px; font-size: .82rem; text-transform: uppercase; letter-spacing: .04em; color: var(--text-muted); }
|
||||
.bank-state-buttons { display: flex; flex-wrap: wrap; gap: 6px; }
|
||||
.bank-difficulty { display: flex; flex-direction: column; gap: 4px; font-size: .82rem; margin-top: 10px; }
|
||||
.bank-category-list { display: flex; flex-direction: column; gap: 2px; max-height: 50vh; overflow-y: auto; }
|
||||
.bank-category-row { display: flex; align-items: center; gap: 4px; }
|
||||
.bank-category-chip { flex: 1; text-align: left; background: none; border: none; border-radius: 6px; padding: 6px 8px; font-size: .82rem; color: var(--text); cursor: pointer; }
|
||||
.bank-category-chip:hover { background: var(--option-hover, #f8fafc); }
|
||||
.bank-category-chip.active { background: var(--option-sel-bg, #eff6ff); color: var(--primary); font-weight: 600; }
|
||||
.bank-main { min-width: 0; }
|
||||
@media (max-width: 820px) {
|
||||
.bank-layout { grid-template-columns: 1fr; }
|
||||
.bank-category-list { max-height: none; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -649,6 +649,7 @@ export default function QuestionBankPage() {
|
|||
const [loading, setLoading] = useState(false)
|
||||
const [categories, setCategories] = useState([])
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [difficulty, setDifficulty] = useState('')
|
||||
const [searchMode, setSearchMode] = useState('hybrid')
|
||||
const [filterCatIds, setFilterCatIds] = useState([])
|
||||
const [showUncategorized, setShowUncategorized] = useState(false)
|
||||
|
|
@ -715,7 +716,7 @@ export default function QuestionBankPage() {
|
|||
clearTimeout(debounceRef.current)
|
||||
debounceRef.current = setTimeout(() => loadQuestions(searchQuery, 0, filterCatIds, showUncategorized, showFavorites, searchMode, pageSize, selectedTagIds), 300)
|
||||
return () => clearTimeout(debounceRef.current)
|
||||
}, [searchQuery, catIdsKey, showUncategorized, showFavorites, showMyQuestions, searchMode, pageSize, tagIdsKey])
|
||||
}, [searchQuery, catIdsKey, showUncategorized, showFavorites, showMyQuestions, searchMode, pageSize, tagIdsKey, difficulty])
|
||||
|
||||
const toggleTag = (tagId) => {
|
||||
setSelectedTagIds(prev => {
|
||||
|
|
@ -737,6 +738,7 @@ export default function QuestionBankPage() {
|
|||
try {
|
||||
const params = {}
|
||||
if (searchQuery.trim()) params.q = searchQuery.trim()
|
||||
if (difficulty) params.difficulty = difficulty
|
||||
if (filterCatIds.length > 0) params.category_ids = filterCatIds.join(',')
|
||||
if (showUncategorized) params.uncategorized = true
|
||||
if (showFavorites) params.favorites_only = true
|
||||
|
|
@ -1011,33 +1013,44 @@ export default function QuestionBankPage() {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Category filter chips */}
|
||||
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 12, paddingBottom: 12, borderBottom: '1px solid var(--border)' }}>
|
||||
<span style={{ fontSize: '0.72rem', fontWeight: 700, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.04em', alignSelf: 'center', marginRight: 4 }}>Filter:</span>
|
||||
<button className={`btn btn-sm ${filterCatIds.length === 0 && !showUncategorized && !showFavorites ? 'btn-primary' : 'btn-secondary'}`}
|
||||
onClick={() => { setFilterCatIds([]); setShowUncategorized(false); setShowFavorites(false) }}>All ({total})</button>
|
||||
<button className={`btn btn-sm ${showFavorites ? 'btn-primary' : 'btn-secondary'}`}
|
||||
onClick={() => { setShowFavorites(v => !v); setFilterCatIds([]); setShowUncategorized(false) }}>⭐ Favorites ({favorites.length})</button>
|
||||
<button className={`btn btn-sm ${showMyQuestions ? 'btn-primary' : 'btn-secondary'}`}
|
||||
onClick={() => { setShowMyQuestions(v => !v); setFilterCatIds([]); setShowUncategorized(false); setShowFavorites(false) }}>My Questions</button>
|
||||
<button className={`btn btn-sm ${showUncategorized ? 'btn-primary' : 'btn-secondary'}`}
|
||||
onClick={() => { setShowUncategorized(v => !v); setFilterCatIds([]); setShowFavorites(false); setShowMyQuestions(false) }}>Uncategorized</button>
|
||||
{categories.map(cat => {
|
||||
const isActive = filterCatIds.includes(cat.id)
|
||||
return (
|
||||
<div key={cat.id} style={{ display: 'flex', alignItems: 'center', gap: 2 }}>
|
||||
<button className={`btn btn-sm ${isActive ? 'btn-primary' : 'btn-secondary'}`}
|
||||
onClick={() => { setFilterCatIds(prev => prev.includes(cat.id) ? prev.filter(c => c !== cat.id) : [...prev, cat.id]); setShowUncategorized(false); setShowFavorites(false) }}>
|
||||
{(cat.breadcrumbs || []).map(c => c.name).join(' › ') || cat.name} <span style={{ opacity: 0.65 }}>({cat.question_count})</span>
|
||||
</button>
|
||||
{isModerator && <button className="btn btn-secondary btn-sm" aria-label={`Edit category ${cat.name}`} onClick={() => { setEditingCategory(cat); setNewCatName(cat.name); setCatParent(cat.parent_id || ''); setShowCatForm(true) }}>Edit</button>}
|
||||
{isModerator && <button aria-label={`Delete category ${cat.name}`} onClick={() => deleteCategory(cat.id)}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#cbd5e1', fontSize: '0.72rem', padding: '2px 3px', lineHeight: 1 }}
|
||||
onMouseEnter={e => e.currentTarget.style.color = '#ef4444'} onMouseLeave={e => e.currentTarget.style.color = '#cbd5e1'}>✕</button>}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
{/* Active tag chips — shown inline with categories */}
|
||||
{/* Filter side box (AMBOSS-style) */}
|
||||
<div className="bank-layout">
|
||||
<aside className="bank-filters card">
|
||||
<h2>Filters</h2>
|
||||
<div className="bank-state-buttons">
|
||||
<button className={`btn btn-sm ${filterCatIds.length === 0 && !showUncategorized && !showFavorites && !showMyQuestions ? 'btn-primary' : 'btn-secondary'}`}
|
||||
onClick={() => { setFilterCatIds([]); setShowUncategorized(false); setShowFavorites(false); setShowMyQuestions(false) }}>All ({total})</button>
|
||||
<button className={`btn btn-sm ${showFavorites ? 'btn-primary' : 'btn-secondary'}`}
|
||||
onClick={() => { setShowFavorites(v => !v); setFilterCatIds([]); setShowUncategorized(false) }}>⭐ Favorites ({favorites.length})</button>
|
||||
<button className={`btn btn-sm ${showMyQuestions ? 'btn-primary' : 'btn-secondary'}`}
|
||||
onClick={() => { setShowMyQuestions(v => !v); setFilterCatIds([]); setShowUncategorized(false); setShowFavorites(false) }}>My Questions</button>
|
||||
<button className={`btn btn-sm ${showUncategorized ? 'btn-primary' : 'btn-secondary'}`}
|
||||
onClick={() => { setShowUncategorized(v => !v); setFilterCatIds([]); setShowFavorites(false); setShowMyQuestions(false) }}>Uncategorized</button>
|
||||
</div>
|
||||
<label className="bank-difficulty">Difficulty
|
||||
<select value={difficulty} onChange={e => setDifficulty(e.target.value)}>
|
||||
<option value="">Any</option><option value="easy">Easy</option>
|
||||
<option value="medium">Medium</option><option value="hard">Hard</option>
|
||||
</select>
|
||||
</label>
|
||||
<h3>Categories</h3>
|
||||
<div className="bank-category-list">
|
||||
{categories.map(cat => {
|
||||
const isActive = filterCatIds.includes(cat.id)
|
||||
return (
|
||||
<div key={cat.id} className="bank-category-row">
|
||||
<button className={`bank-category-chip ${isActive ? 'active' : ''}`}
|
||||
onClick={() => { setFilterCatIds(prev => prev.includes(cat.id) ? prev.filter(c => c !== cat.id) : [...prev, cat.id]); setShowUncategorized(false); setShowFavorites(false) }}>
|
||||
{(cat.breadcrumbs || []).map(c => c.name).join(' › ') || cat.name} <span style={{ opacity: 0.65 }}>({cat.question_count})</span>
|
||||
</button>
|
||||
{isModerator && <button className="btn btn-secondary btn-sm" aria-label={`Edit category ${cat.name}`} onClick={() => { setEditingCategory(cat); setNewCatName(cat.name); setCatParent(cat.parent_id || ''); setShowCatForm(true) }}>Edit</button>}
|
||||
{isModerator && <button aria-label={`Delete category ${cat.name}`} onClick={() => deleteCategory(cat.id)}
|
||||
style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#cbd5e1', fontSize: '0.72rem', padding: '2px 3px', lineHeight: 1 }}
|
||||
onMouseEnter={e => e.currentTarget.style.color = '#ef4444'} onMouseLeave={e => e.currentTarget.style.color = '#cbd5e1'}>✕</button>}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{selectedTagIds.map(tid => {
|
||||
const allTags = [...(tags.subjects || []), ...(tags.diseases || []), ...(tags.keywords || [])]
|
||||
const tag = allTags.find(t => t.id === tid)
|
||||
|
|
@ -1056,18 +1069,19 @@ export default function QuestionBankPage() {
|
|||
Clear tags
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{(tags.subjects?.length > 0 || tags.diseases?.length > 0 || tags.keywords?.length > 0) && (
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<button className="btn btn-sm btn-secondary" onClick={() => setShowTags(v => !v)}
|
||||
style={{ fontSize: '0.78rem', marginBottom: showTags ? 6 : 0 }}>
|
||||
🏷 Browse Tags {showTags ? '▲' : '▼'}
|
||||
</button>
|
||||
{showTags && <TagBrowser tags={tags} selectedTagIds={selectedTagIds} toggleTag={toggleTag} />}
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
|
||||
{/* Tag browser — collapsible */}
|
||||
{(tags.subjects?.length > 0 || tags.diseases?.length > 0 || tags.keywords?.length > 0) && (
|
||||
<div style={{ marginBottom: 12 }}>
|
||||
<button className="btn btn-sm btn-secondary" onClick={() => setShowTags(v => !v)}
|
||||
style={{ fontSize: '0.78rem', marginBottom: showTags ? 6 : 0 }}>
|
||||
🏷 Browse Tags {showTags ? '▲' : '▼'}
|
||||
</button>
|
||||
{showTags && <TagBrowser tags={tags} selectedTagIds={selectedTagIds} toggleTag={toggleTag} />}
|
||||
</div>
|
||||
)}
|
||||
{/* Main column */}
|
||||
<div className="bank-main">
|
||||
|
||||
{/* AI question classification is intentionally not exposed in the UI;
|
||||
it remains available through the command line/Celery interface only. */}
|
||||
|
|
@ -1217,6 +1231,8 @@ export default function QuestionBankPage() {
|
|||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue