diff --git a/docker-compose.yml b/docker-compose.yml index 11b3b1a..29e5baa 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -28,6 +28,8 @@ services: build: ./backend env_file: - ./backend/.env + environment: + - ANONYMIZED_TELEMETRY=False volumes: - uploads_data:/app/uploads - chroma_data:/app/chroma_data @@ -43,6 +45,8 @@ services: command: celery -A app.tasks worker --loglevel=info --concurrency=2 env_file: - ./backend/.env + environment: + - ANONYMIZED_TELEMETRY=False volumes: - uploads_data:/app/uploads - chroma_data:/app/chroma_data diff --git a/frontend/index.html b/frontend/index.html index 20e5c54..df9e727 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -5,6 +5,10 @@ PedQuiz + + + +
diff --git a/frontend/src/components/Navbar.jsx b/frontend/src/components/Navbar.jsx index 4c7f0dc..9b2d383 100644 --- a/frontend/src/components/Navbar.jsx +++ b/frontend/src/components/Navbar.jsx @@ -1,25 +1,152 @@ -import { Link } from 'react-router-dom' +import { useState, useEffect } from 'react' +import { Link, useLocation } from 'react-router-dom' import { useAuth } from '../context/AuthContext' +import api from '../api/client' + +function JobsBadge() { + const [activeJobs, setActiveJobs] = useState([]) + const [open, setOpen] = useState(false) + + useEffect(() => { + // Load tracked jobs from localStorage + const load = () => { + const stored = JSON.parse(localStorage.getItem('pedquiz_jobs') || '[]') + setActiveJobs(stored.filter(j => j.status !== 'completed' && j.status !== 'failed')) + } + load() + const interval = setInterval(load, 3000) + return () => clearInterval(interval) + }, []) + + if (activeJobs.length === 0) return null + + return ( +
+ + {open && ( +
+ {activeJobs.map(job => ( +
+
{job.title}
+
{job.lastStep || 'Processing…'}
+
+ ))} + setOpen(false)}> + View all extractions β†’ + +
+ )} +
+ ) +} export default function Navbar() { const { user, logout } = useAuth() + const [menuOpen, setMenuOpen] = useState(false) + const location = useLocation() const isModerator = user?.role === 'admin' || user?.role === 'moderator' + // Close menu on route change + useEffect(() => { setMenuOpen(false) }, [location.pathname]) + + const navLinks = user ? [ + { to: '/', label: 'Dashboard' }, + { to: '/quizzes', label: 'Quizzes' }, + { to: '/question-bank', label: 'Question Bank' }, + ...(isModerator ? [{ to: '/upload', label: 'Upload PDF' }] : []), + { to: '/settings', label: 'βš™ Settings' }, + ] : [] + return (
- 🩺 PedQuiz + setMenuOpen(false)}>🩺 PedQuiz + {user && ( - + <> + {/* Desktop nav */} + + + {/* Mobile: jobs + hamburger */} +
+ + +
+ )}
+ + {/* Mobile dropdown */} + {user && menuOpen && ( +
+
+ {navLinks.map(l => ( + + {l.label} + + ))} + +
+
+ )}
) } diff --git a/frontend/src/index.css b/frontend/src/index.css index e149321..31afc83 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -2,9 +2,9 @@ :root { --bg: #f0f4f8; --card-bg: #ffffff; - --card-border: 1px solid rgba(0,0,0,0.05); - --card-shadow: 0 1px 6px rgba(0,0,0,0.06), 0 0 0 1px rgba(0,0,0,0.03); - --card-radius: 14px; + --card-border: 1px solid rgba(0,0,0,0.07); + --card-shadow: 0 1px 4px rgba(0,0,0,0.07), 0 4px 16px rgba(0,0,0,0.04); + --card-radius: 12px; --primary: #2563eb; --primary-hover: #1d4ed8; --primary-fg: #ffffff; @@ -30,68 +30,79 @@ --badge-radius: 12px; } -/* ── Warm Brown / Parchment theme ────────────────────────────── */ +/* ── Warm / Literary theme ────────────────────────────────────── */ [data-theme="markdown"] { - --bg: #f2ebe0; - --card-bg: #faf6ef; - --card-border: 1px solid #d6c9b6; - --card-shadow: 0 1px 3px rgba(80,50,20,0.07); - --card-radius: 8px; - --primary: #7c4a1e; - --primary-hover: #633c18; + --bg: #f5f0e8; + --card-bg: #fdfaf4; + --card-border: 1px solid #e0d5c5; + --card-shadow: 0 2px 8px rgba(60,40,10,0.06); + --card-radius: 10px; + --primary: #8b5a2b; + --primary-hover: #6d4420; --primary-fg: #fdf8f0; - --text: #2c1a0e; + --text: #1e140a; --text-muted: #7a5c42; - --text-subtle: #a8886a; - --border: #d6c9b6; - --input-bg: #fdf8f0; - --option-bg: #fdf8f0; - --option-hover: #f5ede0; - --option-sel-bg: #f5e6d3; - --option-sel-bd: #7c4a1e; - --correct-bg: #e6f4e6; - --correct-bd: #8cbf8c; - --correct-fg: #1a4d1a; + --text-subtle: #aa8a6a; + --border: #e0d5c5; + --input-bg: #fdfaf4; + --option-bg: #fdfaf4; + --option-hover: #f7f0e4; + --option-sel-bg: #f2e4d0; + --option-sel-bd: #8b5a2b; + --correct-bg: #e8f5e8; + --correct-bd: #8abf8a; + --correct-fg: #1a4a1a; --wrong-bg: #fdecea; - --wrong-bd: #d9908a; - --wrong-fg: #6b1e1e; - --expl-bg: #f8f3ea; - --expl-bd: #b8945a; - --navbar-bg: #2c1a0e; - --navbar-fg: #f2e8d8; + --wrong-bd: #d99090; + --wrong-fg: #6b1a1a; + --expl-bg: #f9f4ea; + --expl-bd: #c4965a; + --navbar-bg: #1e140a; + --navbar-fg: #f0e8d8; --badge-radius: 4px; - --font-body: Georgia, 'Times New Roman', 'Palatino', serif; - --font-heading: Georgia, 'Times New Roman', serif; + --font-body: 'Source Serif 4', Georgia, serif; + --font-heading: 'Playfair Display', 'Source Serif 4', serif; + --font-ui: 'Inter', system-ui, sans-serif; } -/* ── Warm Brown theme overrides ─────────────────────────────── */ +/* ── Warm theme overrides ────────────────────────────────────── */ body[data-theme="markdown"] { - font-family: var(--font-body); - font-size: 16px; - line-height: 1.75; + font-family: var(--font-ui); + font-size: 15.5px; + line-height: 1.7; } -[data-theme="markdown"] .navbar .logo { color: #d4a96a; } -[data-theme="markdown"] h1, -[data-theme="markdown"] h2, -[data-theme="markdown"] h3 { - font-family: var(--font-heading); - font-weight: 700; +[data-theme="markdown"] .navbar .logo { color: #e0a84a; font-family: var(--font-heading); letter-spacing: 0; } +[data-theme="markdown"] h1 { font-family: var(--font-heading); font-weight: 700; letter-spacing: -0.02em; } +[data-theme="markdown"] h2, [data-theme="markdown"] .card h2 { font-family: var(--font-heading); font-weight: 600; } +[data-theme="markdown"] h3 { font-family: var(--font-ui); font-weight: 600; } +[data-theme="markdown"] .card h2 { border-bottom: 1px solid var(--border); padding-bottom: 10px; margin-bottom: 18px; } +[data-theme="markdown"] .question-card { border-left: 3px solid var(--primary); } +[data-theme="markdown"] .question-card h3 { + font-family: var(--font-body); + font-size: 1.05rem; + line-height: 1.7; + font-weight: 400; + font-style: normal; color: var(--text); } -[data-theme="markdown"] .card h2 { border-bottom: 1px solid var(--border); padding-bottom: 10px; } -[data-theme="markdown"] .question-card { border-left: 4px solid var(--primary); } -[data-theme="markdown"] .question-card h3 { font-family: var(--font-heading); font-size: 1.05rem; line-height: 1.65; } -[data-theme="markdown"] .option { border-radius: 6px; font-size: 0.95rem; font-family: inherit; } -[data-theme="markdown"] .option-letter { border-radius: 3px; font-family: Georgia, serif; font-style: italic; } -[data-theme="markdown"] .explanation { font-family: inherit; line-height: 1.85; border-left: 3px solid var(--expl-bd); background: var(--expl-bg); } -[data-theme="markdown"] .btn { font-family: inherit; } -[data-theme="markdown"] .score-display .score-value { letter-spacing: -2px; font-family: Georgia, serif; } +[data-theme="markdown"] .option { border-radius: 6px; font-size: 0.94rem; } +[data-theme="markdown"] .option-letter { border-radius: 3px; font-family: var(--font-ui); font-weight: 600; } +[data-theme="markdown"] .explanation { + font-family: var(--font-body); + font-size: 0.9rem; + line-height: 1.85; + border-left: 3px solid var(--expl-bd); + background: var(--expl-bg); + color: var(--text-muted); +} +[data-theme="markdown"] .btn { font-family: var(--font-ui); font-weight: 500; } +[data-theme="markdown"] .score-display .score-value { font-family: var(--font-heading); letter-spacing: -3px; } /* ── Reset ──────────────────────────────────────────────────── */ * { margin: 0; padding: 0; box-sizing: border-box; } body { - font-family: ui-sans-serif, system-ui, -apple-system, 'Segoe UI', sans-serif; + font-family: 'Inter', ui-sans-serif, system-ui, -apple-system, sans-serif; background: var(--bg); color: var(--text); line-height: 1.6; @@ -107,11 +118,11 @@ body { .navbar .container { display: flex; justify-content: space-between; align-items: center; height: 52px; } .navbar .logo { font-size: 1.15rem; font-weight: 700; color: #60a5fa; text-decoration: none; letter-spacing: -0.01em; white-space: nowrap; } [data-theme="markdown"] .navbar .logo { color: #d4a96a; } -.navbar nav { display: flex; gap: 4px; align-items: center; overflow-x: auto; scrollbar-width: none; -ms-overflow-style: none; } -.navbar nav::-webkit-scrollbar { display: none; } -.navbar a { color: var(--navbar-fg); text-decoration: none; font-size: 0.83rem; opacity: 0.7; padding: 6px 10px; border-radius: 6px; white-space: nowrap; transition: opacity 0.15s, background 0.15s; } -.navbar a:hover { opacity: 1; background: rgba(255,255,255,0.08); } -.navbar button { background: transparent; border: 1px solid rgba(255,255,255,0.2); color: var(--navbar-fg); padding: 5px 14px; border-radius: 6px; cursor: pointer; font-size: 0.82rem; white-space: nowrap; } +.nav-desktop { display: flex; gap: 4px; align-items: center; } +.nav-mobile-controls { display: none; align-items: center; gap: 8px; } +.navbar a { color: var(--navbar-fg); text-decoration: none; font-size: 0.83rem; padding: 6px 10px; border-radius: 6px; white-space: nowrap; transition: opacity 0.15s, background 0.15s; } +.navbar a:hover { background: rgba(255,255,255,0.09); opacity: 1 !important; } +.navbar button { background: transparent; border: 1px solid rgba(255,255,255,0.2); color: var(--navbar-fg); padding: 5px 14px; border-radius: 6px; cursor: pointer; font-size: 0.82rem; white-space: nowrap; font-family: inherit; } /* ── Cards ──────────────────────────────────────────────────── */ .card { @@ -305,8 +316,8 @@ body { .card { padding: 18px 16px; } .quiz-nav { flex-wrap: wrap; } .quiz-nav-numbers { order: 3; width: 100%; margin-top: 8px; } - .navbar .container { height: 48px; } - .navbar .logo { font-size: 1rem; } - .navbar a { font-size: 0.78rem; padding: 5px 7px; } - .navbar button { padding: 4px 10px; font-size: 0.78rem; } + .navbar .container { height: 52px; } + /* Mobile: show hamburger, hide desktop nav */ + .nav-desktop { display: none; } + .nav-mobile-controls { display: flex; } } diff --git a/frontend/src/pages/DocumentDetailPage.jsx b/frontend/src/pages/DocumentDetailPage.jsx index 44acb78..2f0494a 100644 --- a/frontend/src/pages/DocumentDetailPage.jsx +++ b/frontend/src/pages/DocumentDetailPage.jsx @@ -14,8 +14,15 @@ function ExtractionProgress({ jobId, onDone, onClose }) { const poll = useCallback(async () => { try { const res = await api.get(`/quizzes/job/${jobId}`) - setSteps(res.data.steps || []) + const newSteps = res.data.steps || [] + setSteps(newSteps) setStatus(res.data.status) + // Update localStorage job tracker + const lastStep = newSteps.length ? newSteps[newSteps.length - 1].message : 'Processing…' + const stored = JSON.parse(localStorage.getItem('pedquiz_jobs') || '[]') + const idx = stored.findIndex(j => j.jobId === jobId) + if (idx >= 0) { stored[idx].status = res.data.status; stored[idx].lastStep = lastStep.slice(0, 60) } + localStorage.setItem('pedquiz_jobs', JSON.stringify(stored)) if (res.data.status === 'completed') { setQuizId(res.data.quiz_id) clearInterval(intervalRef.current) @@ -179,6 +186,11 @@ export default function DocumentDetailPage() { }) // Async: show progress panel if (res.data.job_id) { + // Track in localStorage so navbar badge can show it + const title = quizTitle || `Quiz: ${sectionName}` + const stored = JSON.parse(localStorage.getItem('pedquiz_jobs') || '[]') + stored.unshift({ jobId: res.data.job_id, title, status: 'running', lastStep: 'Starting…', ts: Date.now() }) + localStorage.setItem('pedquiz_jobs', JSON.stringify(stored.slice(0, 10))) setActiveJob({ jobId: res.data.job_id, sectionName }) // If already completed (sync fallback), navigate directly if (res.data.status === 'completed' && res.data.quiz_id) {