Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions e2e/session-runner.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,10 @@ test.describe.serial("E2E: Full Retrieval Session Runner", () => {
page.getByRole("button", { name: "✓ Correct" }).click(),
]);

// Prompt should advance
// The review panel keeps the just-answered prompt on the card — the
// prompt advances only when the student moves on.
await expect(page.getByText("PROMPT 1 / 3")).toBeVisible({ timeout: 10_000 });
await page.getByRole("button", { name: /next prompt/i }).click();
await expect(page.getByText("PROMPT 2 / 3")).toBeVisible({ timeout: 10_000 });
});

Expand Down Expand Up @@ -140,8 +143,14 @@ test.describe.serial("E2E: Full Retrieval Session Runner", () => {
page.getByRole("button", { name: /save.*next/i }).click(),
]);

// Prompt should advance — variant injection extends deck from 3 to 4
// The review panel keeps the just-answered prompt (snapshotted at submit
// time, before variant injection grew the deck) on the card.
await expect(page.getByText("PROMPT 2 / 3")).toBeVisible({ timeout: 10_000 });
await page.getByRole("button", { name: /next prompt/i }).click();
// Prompt should advance — variant injection extends deck from 3 to 4,
// and the runner names the growth instead of changing the count silently.
await expect(page.getByText("PROMPT 3 / 4")).toBeVisible({ timeout: 10_000 });
await expect(page.getByText("+1 repair added to this session")).toBeVisible();
});

test("refresh page mid-run preserves progress", async ({ page }) => {
Expand Down
2 changes: 1 addition & 1 deletion src/app/chat/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -241,7 +241,7 @@ export default function ChatPage() {
{/* Header */}
<div style={headerStyle}>
<div style={{ display: "flex", alignItems: "baseline", justifyContent: "space-between" }}>
<h1 style={titleStyle}>Source Chat</h1>
<h1 style={titleStyle}>Chat</h1>
{messages.length > 0 && (
<button
onClick={clearChat}
Expand Down
8 changes: 5 additions & 3 deletions src/app/flashcards/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -158,14 +158,16 @@ export default function FlashcardsPage() {
return () => { mounted = false; };
}, []);

// Fetch decks when course changes
// Fetch decks when course changes. Deck listing is scoped by course only
// (not exam) so the decks shown here always match the per-course deck/due
// counts reported by /api/learn — a deck generated without an exam name
// must not vanish when an exam-specific option is selected.
useEffect(() => {
if (!selectedCourse) return;
let mounted = true;
setLoadingDecks(true);
const [courseName, examName] = selectedCourse.split("||");
const [courseName] = selectedCourse.split("||");
const params = new URLSearchParams({ course_name: courseName });
if (examName) params.set("exam_name", examName);
apiGet(`/api/flashcards?${params.toString()}`)
.then((data) => {
if (mounted && data.decks) setDecks(data.decks);
Expand Down
89 changes: 13 additions & 76 deletions src/app/learn/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@

import { useState, useEffect } from "react";
import Link from "next/link";
import { getActiveCourse, setActiveCourse } from "@/lib/client-utils";
import { getActiveCourse, setActiveCourse, MODE_LABELS } from "@/lib/client-utils";
import { apiGet } from "@/lib/client-api";

/** Pretty label for a session mode (e.g. "RETRIEVAL" → "Retrieval"). */
function modeLabel(mode: string): string {
return MODE_LABELS[mode] || mode.replace(/_/g, " ");
}

/** Render a mastery key / objective slug (e.g. "photosynthesis_light_reactions") as a clean label. */
function prettifyObjectiveKey(key: string): string {
return key
Expand All @@ -26,7 +31,6 @@ interface CourseData {
interface LearnData {
courses: CourseData[];
hasCourses: boolean;
weeklyXp: number;
}

interface Recommendation {
Expand Down Expand Up @@ -100,7 +104,7 @@ export default function LearnPage() {
setSelectedCourse(match ? match.courseName : d.courses[0].courseName);
}
})
.catch(() => setData({ courses: [], hasCourses: false, weeklyXp: 0 }))
.catch(() => setData({ courses: [], hasCourses: false }))
.finally(() => setLoading(false));
}, []);

Expand Down Expand Up @@ -199,28 +203,6 @@ export default function LearnPage() {
)}
</section>

{/* Course stats row */}
<section style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(120px, 1fr))", gap: "0.5rem", marginBottom: "1.5rem" }}>
<div style={miniStatStyle}>
<span style={miniStatNumStyle}>{course.docCount}</span>
<span style={miniStatLabelStyle}>Docs</span>
</div>
<div style={miniStatStyle}>
<span style={{ ...miniStatNumStyle, color: "var(--color-info)" }}>{course.deckCount}</span>
<span style={miniStatLabelStyle}>Decks</span>
</div>
<div style={miniStatStyle}>
<span style={{ ...miniStatNumStyle, color: course.dueCardCount > 0 ? "var(--color-warning)" : "var(--color-success)" }}>
{course.dueCardCount}
</span>
<span style={miniStatLabelStyle}>Due Cards</span>
</div>
<div style={miniStatStyle}>
<span style={{ ...miniStatNumStyle, color: "var(--color-review)" }}>{course.guideCount}</span>
<span style={miniStatLabelStyle}>Guides</span>
</div>
</section>

{/* Mastery-driven recommendation */}
{recs && (
<section style={{ ...recommendationStyle, borderLeftColor: "var(--color-primary)" }}>
Expand All @@ -240,7 +222,7 @@ export default function LearnPage() {
)}
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem", marginBottom: "0.3rem" }}>
<span style={{ color: "var(--color-primary)", fontWeight: 700, fontSize: "0.95rem" }}>
Recommended: {recs.next_session.mode.replace(/_/g, " ")}
Recommended: {modeLabel(recs.next_session.mode)}
</span>
</div>
<p style={{ color: "var(--color-text-dim)", fontSize: "0.8rem", margin: "0 0 0.5rem", lineHeight: 1.4 }}>
Expand All @@ -261,30 +243,14 @@ export default function LearnPage() {
disabled={startingSession}
style={{ ...primaryBtnStyle, display: "inline-block", marginTop: "0.5rem", fontSize: "0.85rem", padding: "0.5rem 1.25rem", opacity: startingSession ? 0.6 : 1, cursor: startingSession ? "wait" : "pointer", border: "none" }}
>
{startingSession ? "Creating session..." : `Start ${recs.next_session.mode.replace(/_/g, " ")} Session`}
{startingSession ? "Creating session..." : `Start ${modeLabel(recs.next_session.mode)} Session`}
</button>
{startError && (
<p style={{ color: "var(--color-error)", fontSize: "0.8rem", margin: "0.4rem 0 0" }}>{startError}</p>
)}
</section>
)}

{/* Due cards alert — only meaningful when the course actually has decks
to review; without decks there is no due queue to send anyone to. */}
{course.deckCount > 0 && course.dueCardCount > 0 && (
<section style={{ ...recommendationStyle, background: "var(--color-bg-warning-tint)", borderLeftColor: "var(--color-warning)" }}>
<div style={{ display: "flex", alignItems: "center", gap: "0.5rem", marginBottom: "0.3rem" }}>
<span style={{ color: "var(--color-warning)", fontSize: "1.1rem" }}>!</span>
<span style={{ color: "var(--color-warning)", fontWeight: 600, fontSize: "0.9rem" }}>
{course.dueCardCount} card{course.dueCardCount !== 1 ? "s" : ""} due for review
</span>
</div>
<p style={{ color: "var(--color-text-dim)", fontSize: "0.8rem", margin: 0 }}>
Reviewing now helps retain information using spaced repetition.
</p>
</section>
)}

{/* Quick actions — focused on what matters now */}
<section style={{ marginBottom: "1.5rem" }}>
<h3 style={sectionLabelStyle}>Quick Actions</h3>
Expand Down Expand Up @@ -339,7 +305,10 @@ export default function LearnPage() {
</div>
</section>

{/* Suggested learning path */}
{/* Suggested learning path — onboarding scaffolding for new courses.
Once materials are uploaded and the first deck exists, the user has
found the core loop and this checklist just repeats the nav. */}
{!(course.processedDocCount > 0 && course.deckCount > 0) && (
<section style={{ marginBottom: "1.5rem" }}>
<h3 style={sectionLabelStyle}>Suggested Learning Path</h3>
<div style={{ display: "flex", flexDirection: "column", gap: "0.5rem" }}>
Expand Down Expand Up @@ -380,12 +349,6 @@ export default function LearnPage() {
/>
</div>
</section>

{/* Weekly XP */}
{data.weeklyXp > 0 && (
<div style={{ textAlign: "center", color: "var(--color-text-dim)", fontSize: "0.8rem", marginTop: "1rem" }}>
{data.weeklyXp} XP earned this week
</div>
)}
</main>
);
Expand Down Expand Up @@ -482,32 +445,6 @@ const sectionLabelStyle: React.CSSProperties = {
letterSpacing: "0.08em",
};

const miniStatStyle: React.CSSProperties = {
display: "flex",
flexDirection: "column",
alignItems: "center",
padding: "0.75rem 0.5rem",
border: "1px solid var(--color-border)",
borderRadius: "var(--radius)",
backgroundColor: "var(--color-bg-card)",
};

const miniStatNumStyle: React.CSSProperties = {
fontSize: "1.3rem",
fontWeight: 700,
color: "var(--color-primary)",
lineHeight: 1,
fontFamily: "var(--font-display)",
};

const miniStatLabelStyle: React.CSSProperties = {
fontSize: "0.65rem",
color: "var(--color-text-faint)",
marginTop: "0.3rem",
textTransform: "uppercase",
letterSpacing: "0.05em",
};

const recommendationStyle: React.CSSProperties = {
background: "var(--color-bg-selected)",
border: "1px solid var(--color-border)",
Expand Down
Loading
Loading