diff --git a/apps/web/src/app/api/library/context/route.ts b/apps/web/src/app/api/library/context/route.ts new file mode 100644 index 0000000..2922fd5 --- /dev/null +++ b/apps/web/src/app/api/library/context/route.ts @@ -0,0 +1,19 @@ +import { handleApi } from "@/lib/api"; +import { + formatLibraryContextForPrompt, + getLibraryContextForTopic, +} from "@/lib/services/library-context"; +import { requireUser } from "@/lib/services/user"; + +export async function GET(request: Request) { + return handleApi(async () => { + const { user } = await requireUser(); + const { searchParams } = new URL(request.url); + const topic = searchParams.get("topic")?.trim() ?? ""; + const payload = await getLibraryContextForTopic(user.id, topic); + return { + ...payload, + formatted: formatLibraryContextForPrompt(payload), + }; + }); +} diff --git a/apps/web/src/app/api/teacher/route.ts b/apps/web/src/app/api/teacher/route.ts index 8ab5241..3493571 100755 --- a/apps/web/src/app/api/teacher/route.ts +++ b/apps/web/src/app/api/teacher/route.ts @@ -1,5 +1,10 @@ import { handleApi } from "@/lib/api"; -import { createTeacherLesson, getTeacherHistory } from "@/lib/services/teacher"; +import { ServiceUnavailableError } from "@/lib/errors"; +import { + createTeacherLesson, + getTeacherHistory, + TeacherAiUnavailableError, +} from "@/lib/services/teacher"; import { requireUser } from "@/lib/services/user"; import { parseJsonBody } from "@/lib/validation"; import { @@ -9,20 +14,17 @@ import { export async function POST(request: Request) { return handleApi(async () => { - // #region agent log - fetch('http://127.0.0.1:7257/ingest/abfa22e4-3d86-4176-9679-7c59b1f0489f',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'04e12c'},body:JSON.stringify({sessionId:'04e12c',location:'api/teacher/route.ts:POST',message:'teacher POST start',data:{},timestamp:Date.now(),hypothesisId:'H1'})}).catch(()=>{}); - // #endregion const parsed = await parseJsonBody(request, teacherCreateSchema); const body = normalizeTeacherCreateBody(parsed); - // #region agent log - fetch('http://127.0.0.1:7257/ingest/abfa22e4-3d86-4176-9679-7c59b1f0489f',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'04e12c'},body:JSON.stringify({sessionId:'04e12c',location:'api/teacher/route.ts:POST',message:'teacher body validated',data:{topicLen:body.topic.length,hasExplorationId:!!body.explorationId},timestamp:Date.now(),hypothesisId:'H1'})}).catch(()=>{}); - // #endregion const { user } = await requireUser(); - const result = await createTeacherLesson(user.id, body); - // #region agent log - fetch('http://127.0.0.1:7257/ingest/abfa22e4-3d86-4176-9679-7c59b1f0489f',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'04e12c'},body:JSON.stringify({sessionId:'04e12c',location:'api/teacher/route.ts:POST',message:'teacher lesson created',data:{source:result.source,lessonId:result.id},timestamp:Date.now(),hypothesisId:'H2'})}).catch(()=>{}); - // #endregion - return result; + try { + return await createTeacherLesson(user.id, body); + } catch (e) { + if (e instanceof TeacherAiUnavailableError) { + throw new ServiceUnavailableError(e.message); + } + throw e; + } }); } diff --git a/apps/web/src/app/teacher/page.tsx b/apps/web/src/app/teacher/page.tsx index cce0d07..3221fd0 100755 --- a/apps/web/src/app/teacher/page.tsx +++ b/apps/web/src/app/teacher/page.tsx @@ -3,29 +3,19 @@ import { useState, useEffect, Suspense } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useSearchParams } from "next/navigation"; +import type { + TeacherLessonContent, + TeacherLessonData, + TeacherDifficulty, + TeacherLength, +} from "@adventureos/shared"; import { AppShell } from "@/components/layout/app-shell"; +import { + resolveResearchAssignmentDisplay, + type ResearchAssignmentDisplay, +} from "@/lib/teacher/teacher-render"; -type LessonContent = { - title?: string; - introduction?: string; - objectives?: string[]; - readingSteps?: string[]; - reflectionPrompt?: string; - flashcards: { front: string; back: string }[]; - quiz: { question: string; options: string[]; answer: number }[]; - assignment: string; -}; - -type Lesson = { - id: string; - topic: string; - content: LessonContent; - status: string; - completedNote?: string | null; - createdAt: string; - source?: "ai" | "fallback"; - fallbackReason?: "offline" | "model_unavailable" | "parse_failed" | "generation_failed" | "timeout"; -}; +type Lesson = TeacherLessonData; export default function TeacherPage() { return ( @@ -39,12 +29,16 @@ function TeacherPageContent() { const searchParams = useSearchParams(); const qc = useQueryClient(); const [topic, setTopic] = useState(""); + const [difficulty, setDifficulty] = useState("beginner"); + const [length, setLength] = useState("standard"); const [explorationId, setExplorationId] = useState(null); const [selectedId, setSelectedId] = useState(null); - const [content, setContent] = useState(null); + const [content, setContent] = useState(null); const [contentSource, setContentSource] = useState<"ai" | "fallback" | null>(null); const [fallbackReason, setFallbackReason] = useState(null); const [revealedQuiz, setRevealedQuiz] = useState>({}); + const [revealedShortAnswers, setRevealedShortAnswers] = useState>({}); + const [showAnswerKey, setShowAnswerKey] = useState(false); const [completionNote, setCompletionNote] = useState(""); useEffect(() => { @@ -63,6 +57,17 @@ function TeacherPageContent() { }, }); + const resetLessonView = () => { + setContent(null); + setContentSource(null); + setFallbackReason(null); + setSelectedId(null); + setRevealedQuiz({}); + setRevealedShortAnswers({}); + setShowAnswerKey(false); + setCompletionNote(""); + }; + const loadLesson = useMutation({ mutationFn: async (id: string) => { const res = await fetch(`/api/teacher/${id}`); @@ -76,6 +81,8 @@ function TeacherPageContent() { setFallbackReason(data.fallbackReason ?? null); setTopic(data.topic); setRevealedQuiz({}); + setRevealedShortAnswers({}); + setShowAnswerKey(false); setCompletionNote(data.completedNote ?? ""); }, }); @@ -87,6 +94,9 @@ function TeacherPageContent() { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ topic, + difficulty, + length, + includeLibraryContext: true, ...(explorationId ? { explorationId } : {}), }), }); @@ -102,6 +112,8 @@ function TeacherPageContent() { setFallbackReason(data.fallbackReason ?? null); setSelectedId(data.id); setRevealedQuiz({}); + setRevealedShortAnswers({}); + setShowAnswerKey(false); qc.invalidateQueries({ queryKey: ["teacher"] }); }, }); @@ -120,120 +132,271 @@ function TeacherPageContent() { onSuccess: () => qc.invalidateQueries({ queryKey: ["teacher"] }), }); + const overview = content?.overview ?? content?.introduction; + const objectives = content?.learningObjectives ?? content?.objectives; + const researchDisplay = content ? resolveResearchAssignmentDisplay(content) : null; + return ( -
+

The Teacher

- Ask the Guide to create flashcards, quizzes, and research assignments on any topic. + A calm mini-lesson with explanation, research steps, library suggestions, homework, and a quiz.

-
+
setTopic(e.target.value)} + disabled={generate.isPending} /> - +
+ + +
+
+ + +
+ {generate.isPending && ( +

+ The Guide is preparing your lesson on “{topic}”… +

+ )} + {generate.isError && (

{generate.error instanceof Error ? generate.error.message - : "Could not generate lesson. Check AI Health."} + : "Could not generate lesson. Check AI Health in Settings."}

)} {contentSource === "fallback" && (

{fallbackReason === "timeout" - ? "AI timed out — your model may be too large for this machine. Try a smaller model (e.g. llama3.2:1b) in Settings or .env." - : fallbackReason === "generation_failed" - ? "AI could not generate a lesson (check that your Ollama model is installed) — showing a basic offline template." - : "AI is offline or unavailable — showing a basic offline lesson template."} + ? "AI timed out — try a smaller model in Settings." + : fallbackReason === "parse_failed" + ? "AI responded but the lesson could not be parsed — showing a basic template." + : fallbackReason === "generation_failed" + ? "AI could not generate a full lesson — showing a basic offline template." + : "AI is offline or unavailable — showing a basic offline lesson template."}

)} {content && (
- {content.title && ( -

{content.title}

- )} - {content.introduction && ( -
-

{content.introduction}

+ {content.title &&

{content.title}

} + + {overview && ( +
+

{overview}

)} - {content.objectives && content.objectives.length > 0 && ( + + {content.whyItMatters && ( +
+

{content.whyItMatters}

+
+ )} + + {objectives && objectives.length > 0 && (
    - {content.objectives.map((o, i) => ( + {objectives.map((o, i) => (
  • {o}
  • ))}
)} - {content.readingSteps && content.readingSteps.length > 0 && ( -
-
    - {content.readingSteps.map((step, i) => ( -
  1. {step}
  2. - ))} -
+ + {content.explanation && ( +
+

{content.explanation}

)} -
-
- {content.flashcards.map((c, i) => ( - - ))} -
-
-
- {content.quiz.map((q, i) => ( - setRevealedQuiz({ ...revealedQuiz, [i]: idx })} - /> - ))} -
-
-

{content.assignment}

- {content.reflectionPrompt && ( -

- Reflection: {content.reflectionPrompt} + + {researchDisplay && ( +

+ +
+ )} + +
+ {content.calibreSuggestions && content.calibreSuggestions.length > 0 ? ( +
    + {content.calibreSuggestions.map((book, i) => ( +
  • + {book.title} + {book.authors.length > 0 && ( + — {book.authors.join(", ")} + )} +

    {book.reason}

    +
  • + ))} +
+ ) : ( +

+ No matching library books were found for this topic.

)} - {selectedId && ( -
-