diff --git a/frontend/src/pages/AnalysisPage.css b/frontend/src/pages/AnalysisPage.css
index 44eb7b2..69cc4f9 100644
--- a/frontend/src/pages/AnalysisPage.css
+++ b/frontend/src/pages/AnalysisPage.css
@@ -1,12 +1,19 @@
/* Session rail beside the analysis, as in a Qbank analysis view. */
-.an-layout { display: grid; grid-template-columns: 260px minmax(0, 1fr); gap: 24px; align-items: start; }
-.an-layout.rail-closed { grid-template-columns: 52px minmax(0, 1fr); }
-.an-rail { position: sticky; top: 16px; background: var(--card-bg); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; }
+/* The rail runs the full height of the viewport against the left edge, rather
+ than sitting in a card, so a long session list is one continuous column. */
+.an-layout { display: grid; grid-template-columns: 280px minmax(0, 1fr); gap: 28px; align-items: start; }
+.an-layout.rail-closed { grid-template-columns: 48px minmax(0, 1fr); }
+.an-rail {
+ position: sticky; top: 0; height: 100vh; display: flex; flex-direction: column;
+ background: var(--card-bg); border-right: 1px solid var(--border);
+ margin-left: calc(50% - 50vw); padding-left: max(0px, calc(50vw - 50%));
+ box-sizing: content-box;
+}
.an-rail-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 12px 14px; border-bottom: 1px solid var(--border); }
.an-rail-head h2 { margin: 0; font-size: .92rem; font-weight: 650; white-space: nowrap; }
.an-layout.rail-closed .an-rail-head h2 { display: none; }
.an-rail-head button { background: none; border: none; cursor: pointer; color: var(--text-muted); font-size: 1.1rem; line-height: 1; padding: 2px 4px; }
-.an-rail-list { list-style: none; margin: 0; padding: 0; max-height: 70vh; overflow-y: auto; }
+.an-rail-list { list-style: none; margin: 0; padding: 0; flex: 1; overflow-y: auto; }
.an-rail-list > li { border-bottom: 1px solid var(--border); }
.an-rail-list > li:last-child { border-bottom: 0; }
.an-rail-list a { display: flex; flex-direction: column; gap: 7px; padding: 12px 14px; text-decoration: none; color: var(--text); }
@@ -17,7 +24,10 @@
@media (max-width: 1100px) {
.an-layout, .an-layout.rail-closed { grid-template-columns: 1fr; }
- .an-rail { position: static; }
+ .an-rail {
+ position: static; height: auto; margin-left: 0; padding-left: 0;
+ border-right: 0; border: 1px solid var(--border); border-radius: 12px;
+ }
.an-rail-list { max-height: 260px; }
}
diff --git a/frontend/src/pages/QuestionBankPage.jsx b/frontend/src/pages/QuestionBankPage.jsx
index 388c15a..d188cb4 100644
--- a/frontend/src/pages/QuestionBankPage.jsx
+++ b/frontend/src/pages/QuestionBankPage.jsx
@@ -6,6 +6,7 @@ import Dialog from '../components/Dialog'
import CategoryTree from '../components/CategoryTree'
import FacetPicker, { FacetRow } from '../components/FacetPicker'
import './CustomQuizPage.css' // facet row + picker panel styles
+import './QuestionBankPage.css'
import { useDialog } from '../hooks/useDialog'
const TeachChat = lazy(() => import('../components/TeachChat'))
diff --git a/frontend/src/pages/QuizzesPage.css b/frontend/src/pages/QuizzesPage.css
index 755ef23..5b1b295 100644
--- a/frontend/src/pages/QuizzesPage.css
+++ b/frontend/src/pages/QuizzesPage.css
@@ -261,3 +261,6 @@
.qz-grid { grid-template-columns: 1fr; }
}
+
+.qz-seemore { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; margin: 6px 0 4px; }
+@media (max-width: 640px) { .qz-seemore .btn { flex: 1; text-align: center; } }
diff --git a/frontend/src/pages/QuizzesPage.jsx b/frontend/src/pages/QuizzesPage.jsx
index 87b844c..70d42bb 100644
--- a/frontend/src/pages/QuizzesPage.jsx
+++ b/frontend/src/pages/QuizzesPage.jsx
@@ -202,7 +202,7 @@ function QuestionStudyModal({ question, query, onClose }) {
}
/* ── One management row: progress, primary action, and the action menu ── */
-function SessionRow({ row, categories, isModerator, onChanged, onDelete }) {
+function SessionRow({ row, isModerator, onChanged, onDelete }) {
const [menuOpen, setMenuOpen] = useState(false)
const [renaming, setRenaming] = useState(false)
const [draftTitle, setDraftTitle] = useState(row.title)
@@ -254,16 +254,6 @@ function SessionRow({ row, categories, isModerator, onChanged, onDelete }) {
finally { setBusy(false) }
}
- const assignCategory = async (value) => {
- closeMenu(); setBusy(true); setError('')
- const catId = value === '' ? null : Number(value)
- try {
- await api.patch(`/categories/quizzes/${row.quiz_id}`, null, { params: catId == null ? {} : { category_id: catId } })
- onChanged(row.quiz_id, { category_id: catId, category_name: categories.find(c => c.id === catId)?.name ?? null })
- } catch (err) { setError(err.response?.data?.detail || 'Could not move this test') }
- finally { setBusy(false) }
- }
-
return (
@@ -347,15 +337,6 @@ function SessionRow({ row, categories, isModerator, onChanged, onDelete }) {
-
>}
{canManage && <>
@@ -398,18 +379,18 @@ function QuizCard({ quiz, isModerator, onOpenSessions }) {
export default function QuizzesPage() {
const [rows, setRows] = useState([])
- const [categories, setCategories] = useState([])
const [loading, setLoading] = useState(true)
const [tab, setTab] = useState('sessions')
const [stateFilter, setStateFilter] = useState('all')
+ // The full history lives on the analysis page; this is a launcher.
+ const [showAllSessions, setShowAllSessions] = useState(false)
+ const SESSION_PREVIEW = 6
const [searchQuery, setSearchQuery] = useState('')
const [searchMode, setSearchMode] = useState('all')
const [searchResults, setSearchResults] = useState(null)
const [searching, setSearching] = useState(false)
const [expandedSearch, setExpandedSearch] = useState(false)
const [studyQuestion, setStudyQuestion] = useState(null)
- const [newCatName, setNewCatName] = useState('')
- const [addingCat, setAddingCat] = useState(false)
const { dialogProps, openAlert } = useDialog()
const debounceRef = useRef(null)
const { user } = useAuth()
@@ -417,13 +398,8 @@ export default function QuizzesPage() {
const isModerator = user?.role === 'admin' || user?.role === 'moderator'
useEffect(() => {
- Promise.all([
- api.get('/quizzes/sessions'),
- api.get('/categories/'),
- ]).then(([sRes, cRes]) => {
- setRows(Array.isArray(sRes.data) ? sRes.data : [])
- setCategories(Array.isArray(cRes.data) ? cRes.data : [])
- }).catch(console.error).finally(() => setLoading(false))
+ api.get('/quizzes/sessions')
+ .then(res => setRows(Array.isArray(res.data) ? res.data : [])).catch(console.error).finally(() => setLoading(false))
}, [])
useEffect(() => {
@@ -451,24 +427,6 @@ export default function QuizzesPage() {
const patchRow = (quizId, patch) =>
setRows(prev => prev.map(r => (r.quiz_id === quizId ? { ...r, ...patch } : r)))
- const addCategory = async () => {
- if (!newCatName.trim()) return
- try {
- const res = await api.post('/categories/', { name: newCatName.trim() })
- setCategories(prev => [...prev, res.data])
- setNewCatName('')
- setAddingCat(false)
- } catch (err) { await openAlert(err.response?.data?.detail || 'Failed to create category', { title: 'Error' }) }
- }
-
- const deleteCategory = async (catId) => {
- try {
- await api.delete(`/categories/${catId}`)
- setCategories(prev => prev.filter(c => c.id !== catId))
- setRows(prev => prev.map(r => (r.category_id === catId ? { ...r, category_id: null, category_name: null } : r)))
- } catch { }
- }
-
const counts = useMemo(() => ({
all: rows.length,
in_progress: rows.filter(r => r.state === 'in_progress').length,
@@ -481,16 +439,21 @@ export default function QuizzesPage() {
[rows, stateFilter],
)
+ const previewRows = useMemo(
+ () => (showAllSessions ? visibleRows : visibleRows.slice(0, SESSION_PREVIEW)),
+ [visibleRows, showAllSessions],
+ )
+
const dayGroups = useMemo(() => {
const groups = []
- for (const row of visibleRows) {
+ for (const row of previewRows) {
const label = dayLabel(row.last_activity)
const last = groups[groups.length - 1]
if (last && last.label === label) last.rows.push(row)
else groups.push({ label, rows: [row] })
}
return groups
- }, [visibleRows])
+ }, [previewRows])
const libraryGroups = useMemo(() => {
const byCat = new Map()
@@ -619,11 +582,6 @@ export default function QuizzesPage() {
- {isModerator && (
-
- )}
{tab === 'sessions' && (
@@ -649,13 +607,32 @@ export default function QuizzesPage() {
{group.label}
{group.rows.map(row => (
-
))}
))}
+ {visibleRows.length > SESSION_PREVIEW && (
+
+ {showAllSessions ? (
+
+ ) : (
+ <>
+
+
+ Full session history →
+
+ >
+ )}
+
+ )}
+
>
)
@@ -677,39 +654,6 @@ export default function QuizzesPage() {
))
)}
- {tab === 'manage' && isModerator && (
-
-
-
Test categories
-
-
- {addingCat && (
-
- setNewCatName(e.target.value)}
- placeholder="Category name…" aria-label="New category name"
- onKeyDown={e => e.key === 'Enter' && addCategory()}
- style={{ flex: 1, minWidth: 170, padding: '7px 12px', border: '1px solid var(--border)', borderRadius: 8, fontSize: '0.875rem', background: 'var(--input-bg)', color: 'var(--text)' }} />
-
-
-
- )}
- {categories.length === 0 ? (
-
No categories yet.
- ) : (
-
- {categories.map(cat => (
-
- {cat.name}
- ({cat.quiz_count})
- deleteCategory(cat.id)} />
-
- ))}
-
- )}
-
- )}
>
)}