import { useEffect, useMemo, useState } from "react"; import { Pill, ProgressBar, SegmentedToggle } from "@stc/ui"; import { api, ApiError, type Status } from "./api"; import { Field, PageHead } from "./components"; interface CourseOpt { id: string; title: string } interface Sources { lessons: { id: string; title: string; type: string; module: string; hasText: boolean }[]; files: string[]; cache: string[] } interface Job { id: string; status: "queued" | "running" | "done" | "failed"; progress: number; message: string | null; error: string | null; result: { location: string; branch?: string; commit?: string; paths: string[]; warnings: string[] } | null } const KINDS = [ { value: "document", label: "Document" }, { value: "exercise", label: "Exercise" }, { value: "questions", label: "Questions" }, ]; const STYLES = [ { value: "pure-snake", label: "Module shape" }, { value: "systemic-design", label: "Observe to Expansion Drop" }, ]; const STYLE_HINT: Record = { "pure-snake": "Pure Snake's shape: why, objectives, concepts, a puzzle before the explanation, exercises with nudge, stronger hint and near-solution, and check questions.", "systemic-design": "Systemic Design's beats: Observe, Predict, Spec, Build, Name it, Expansion Drop, Further reading.", }; export default function GeneratePage() { const [courses, setCourses] = useState(null); const [denied, setDenied] = useState(null); const [enabled, setEnabled] = useState(true); const [courseId, setCourseId] = useState(""); const [src, setSrc] = useState(null); const [kind, setKind] = useState("document"); const [style, setStyle] = useState("pure-snake"); const [topic, setTopic] = useState(""); const [instructions, setInstructions] = useState(""); const [lessons, setLessons] = useState>(new Set()); const [files, setFiles] = useState>(new Set()); const [cache, setCache] = useState>(new Set()); const [pasted, setPasted] = useState(""); const [filter, setFilter] = useState(""); const [job, setJob] = useState(null); const [error, setError] = useState(null); useEffect(() => { api("/api/ai/studio/courses").then((c) => { setCourses(c); if (c[0]) setCourseId(c[0].id); }, (e: ApiError) => setDenied(e.status === 401 ? "Sign in as an author to generate drafts." : e.message)); api("/api/ai/status").then((s) => setEnabled(s.enabled), () => {}); }, []); useEffect(() => { if (!courseId) return; setSrc(null); setLessons(new Set()); setFiles(new Set()); setCache(new Set()); api(`/api/ai/studio/sources?courseId=${encodeURIComponent(courseId)}`).then(setSrc, (e) => setError(e.message)); }, [courseId]); useEffect(() => { if (!job || job.status === "done" || job.status === "failed") return; const t = setTimeout(() => api(`/api/ai/studio/jobs/${job.id}`).then(setJob, (e) => setError(e.message)), 1000); return () => clearTimeout(t); }, [job]); const toggle = (set: Set, setter: (s: Set) => void, v: string) => { const n = new Set(set); if (n.has(v)) n.delete(v); else n.add(v); setter(n); }; const byModule = useMemo(() => { const m = new Map(); for (const l of src?.lessons ?? []) m.set(l.module, [...(m.get(l.module) ?? []), l]); return [...m]; }, [src]); const shownFiles = (src?.files ?? []).filter((f) => !filter || f.toLowerCase().includes(filter.toLowerCase())).slice(0, 200); const sourceCount = lessons.size + files.size + cache.size + (pasted.trim() ? 1 : 0); const generate = async () => { setError(null); setJob(null); const sources = [ ...[...lessons].map((lessonId) => ({ kind: "lesson", lessonId })), ...[...files].map((path) => ({ kind: "file", path })), ...[...cache].map((path) => ({ kind: "cache", path })), ...(pasted.trim() ? [{ kind: "text", label: "Pasted text", text: pasted }] : []), ]; try { const { jobId } = await api<{ jobId: string }>("/api/ai/studio/generate", { method: "POST", body: { courseId, kind, style, topic, instructions, sources } }); setJob({ id: jobId, status: "queued", progress: 0, message: "Queued", error: null, result: null }); } catch (e) { setError((e as Error).message); } }; if (denied) return

{denied}

; if (!courses) return
; if (!courses.length) return

You are not an author of any course yet.

; const running = job && (job.status === "queued" || job.status === "running"); return (

Drafts a document, an exercise or knowledge-check questions from material you choose. The result is written as draft files for you to review and edit. Nothing is published.

{!enabled ?

AI is not set up. Ask the owner to configure a provider, or add your own key in Account, AI.

: null}
{ e.preventDefault(); generate(); }}>
setTopic(e.target.value)} placeholder="For example: Folding time" required />

{STYLE_HINT[style]}

Material

Lessons and transcripts, code and text files, slide text from imports, or text you paste. {sourceCount ? `${sourceCount} selected.` : ""} {!src ?

Loading…

: ( <>
{byModule.map(([mod, ls]) => (
{mod}
{ls.map((l) => ( ))}
))}
{src.files.length ? ( <> setFilter(e.target.value)} placeholder="Filter files" />
{shownFiles.map((f) => ( ))}
) : null} {src.cache.length ? (
Slide text and transcripts
{src.cache.map((f) => ( ))}
) : null} )}