From 82af4c183000a737d109a96932c156c4005432ae Mon Sep 17 00:00:00 2001 From: iamdoubz <> Date: Mon, 29 Jun 2026 10:57:53 -0500 Subject: [PATCH] AI Creator improvements and admin functions --- src/app/admin/AdminCreations.tsx | 74 +++++++++++++++-- src/app/api/admin/create/[id]/route.ts | 15 +++- src/app/api/create/route.ts | 4 +- src/app/create/CreateForm.tsx | 9 +- src/app/create/page.tsx | 6 +- src/app/learn/[level]/[slug]/page.tsx | 28 ++++--- src/app/learn/page.tsx | 35 ++++---- src/lib/createdLessons.ts | 90 +++++++++++++------- src/lib/curriculum.ts | 5 ++ src/lib/vectorize.ts | 110 +++++++++++++------------ 10 files changed, 247 insertions(+), 129 deletions(-) diff --git a/src/app/admin/AdminCreations.tsx b/src/app/admin/AdminCreations.tsx index 608c787..ffc1d9f 100644 --- a/src/app/admin/AdminCreations.tsx +++ b/src/app/admin/AdminCreations.tsx @@ -1,5 +1,5 @@ "use client"; -import { useState } from "react"; +import { useEffect, useRef, useState } from "react"; import { useRouter } from "next/navigation"; interface Row { @@ -16,15 +16,41 @@ interface Row { export default function AdminCreations({ review, ready }: { review: Row[]; ready: Row[] }) { const router = useRouter(); const [busy, setBusy] = useState(null); + const [lightbox, setLightbox] = useState(null); + const [editing, setEditing] = useState(null); + const editVal = useRef(""); - async function act(id: number, action: "approve" | "block" | "promote" | "regenerate") { + // Auto-update: while anything is generating, poll its status and refresh when it changes + // (so a Regenerate's new image appears on screen without a manual reload). + useEffect(() => { + const working = review.filter((r) => r.status === "pending" || r.status === "generating"); + if (working.length === 0) return; + const iv = setInterval(async () => { + for (const r of working) { + try { + const res = await fetch(`/api/create/${r.id}`); + const d = await res.json(); + if (d.status && d.status !== r.status) { + router.refresh(); + break; + } + } catch { + /* keep polling */ + } + } + }, 3000); + return () => clearInterval(iv); + }, [review, router]); + + async function act(id: number, action: "approve" | "block" | "promote" | "regenerate" | "rename", subject?: string) { setBusy(id); try { await fetch(`/api/admin/create/${id}`, { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ action }), + body: JSON.stringify(subject !== undefined ? { action, subject } : { action }), }); + setEditing(null); router.refresh(); } finally { setBusy(null); @@ -37,12 +63,18 @@ export default function AdminCreations({ review, ready }: { review: Row[]; ready const thumb = (r: Row) => r.image ? ( - {`${r.subject} + {`${r.subject} setLightbox(r.image)} + title="Click to view full screen" + style={{ width: 96, height: 72, objectFit: "contain", background: "#fff", border: "1px solid var(--line)", borderRadius: 8, flex: "0 0 auto", cursor: "zoom-in" }} + /> ) : (
{r.emoji}
); - const previewHref = (r: Row) => `/learn/${r.level}/${r.slug}-trace`; + const previewHref = (r: Row) => `/learn/${r.level}/${r.slug}-outline`; const card = (r: Row, kind: "queue" | "ready") => { const generated = !!r.image && (r.status === "review" || r.status === "ready"); @@ -50,10 +82,22 @@ export default function AdminCreations({ review, ready }: { review: Row[]; ready return (
{thumb(r)} -
- {r.emoji} {r.subject} · {r.level} +
+ {editing === r.id ? ( +
+ (editVal.current = e.target.value)} maxLength={30} style={{ flex: 1, minWidth: 120 }} /> + + +
+ ) : ( + + {r.emoji} {r.subject}{" "} + + · {r.level} + + )}
- {working ? "🎨 Drawing… (refresh to update)" : r.status === "review" ? "🔎 Awaiting approval" : r.status === "needs_review" ? "⏳ Needs generation" : r.status === "failed" ? `⚠️ Failed: ${r.error || "unknown error"}` : r.status === "ready" ? "✓ Ready" : r.status} + {working ? "🎨 Drawing… (updates automatically)" : r.status === "review" ? "🔎 Awaiting approval" : r.status === "needs_review" ? "⏳ Needs generation" : r.status === "failed" ? `⚠️ Failed: ${r.error || "unknown error"}` : r.status === "ready" ? "✓ Ready" : r.status}
@@ -62,7 +106,7 @@ export default function AdminCreations({ review, ready }: { review: Row[]; ready )} {(generated || r.status === "failed") && ( - + )} {kind === "ready" && ( @@ -89,6 +133,18 @@ export default function AdminCreations({ review, ready }: { review: Row[]; ready {ready.map((r) => card(r, "ready"))} )} + + {lightbox && ( +
setLightbox(null)} + role="dialog" + aria-modal="true" + style={{ position: "fixed", inset: 0, background: "rgba(20,16,30,0.82)", display: "grid", placeItems: "center", padding: 20, zIndex: 100, cursor: "zoom-out" }} + > + Generated line art, full size + +
+ )}
); } diff --git a/src/app/api/admin/create/[id]/route.ts b/src/app/api/admin/create/[id]/route.ts index 6b4215f..74d4cd2 100644 --- a/src/app/api/admin/create/[id]/route.ts +++ b/src/app/api/admin/create/[id]/route.ts @@ -1,8 +1,10 @@ import { NextResponse } from "next/server"; import { requireAdmin } from "@/lib/session"; -import { getCreatedById, updateCreatedStatus, promoteCreated, processCreation, approveCreated } from "@/lib/createdLessons"; +import { getCreatedById, updateCreatedStatus, promoteCreated, processCreation, approveCreated, updateCreatedSubject } from "@/lib/createdLessons"; +import { sanitizeSubject } from "@/lib/moderation"; +import { resolvePrompt } from "@/lib/comfyui"; -// Admin review queue: approve (generate), block, or promote a creation to the global curriculum. +// Admin review queue: approve, regenerate, rename, block, or promote a creation. export async function POST(req: Request, ctx: { params: Promise<{ id: string }> }) { const admin = await requireAdmin(); if (!admin) return NextResponse.json({ error: "Not authorized." }, { status: 403 }); @@ -11,7 +13,7 @@ export async function POST(req: Request, ctx: { params: Promise<{ id: string }> const row = getCreatedById(Number(id)); if (!row) return NextResponse.json({ error: "Not found." }, { status: 404 }); - let body: { action?: string }; + let body: { action?: string; subject?: string }; try { body = await req.json(); } catch { @@ -35,6 +37,13 @@ export async function POST(req: Request, ctx: { params: Promise<{ id: string }> updateCreatedStatus(row.id, "pending"); void processCreation(row.id); return NextResponse.json({ ok: true, status: "generating" }); + case "rename": { + // Admins can fix the subject wording; the new prompt is used on the next regenerate. + const subject = sanitizeSubject(body.subject || ""); + if (!subject) return NextResponse.json({ error: "Enter a subject." }, { status: 400 }); + updateCreatedSubject(row.id, subject, resolvePrompt(subject)); + return NextResponse.json({ ok: true, subject }); + } case "block": updateCreatedStatus(row.id, "blocked"); return NextResponse.json({ ok: true, status: "blocked" }); diff --git a/src/app/api/create/route.ts b/src/app/api/create/route.ts index a19afd7..b285e97 100644 --- a/src/app/api/create/route.ts +++ b/src/app/api/create/route.ts @@ -3,8 +3,8 @@ import { requireCreator } from "@/lib/session"; import { isCreateEnabled, resolvePrompt } from "@/lib/comfyui"; import { moderateSubject } from "@/lib/moderation"; import { createCreatedLesson, processCreation, listCreatedForUser } from "@/lib/createdLessons"; +import { CREATABLE_LEVELS } from "@/lib/curriculum"; -const LEVELS = new Set(["early-beginner", "beginner"]); const DAILY_LIMIT = 20; export async function POST(req: Request) { @@ -20,7 +20,7 @@ export async function POST(req: Request) { } const level = body.level || ""; - if (!LEVELS.has(level)) return NextResponse.json({ error: "Pick a level." }, { status: 400 }); + if (!CREATABLE_LEVELS.includes(level)) return NextResponse.json({ error: "Pick a level." }, { status: 400 }); // Simple per-user daily cap (bounds abuse + GPU cost). const todays = listCreatedForUser(user.id, level).filter((r) => r.created_at.slice(0, 10) === new Date().toISOString().slice(0, 10)); diff --git a/src/app/create/CreateForm.tsx b/src/app/create/CreateForm.tsx index e883039..8968a3b 100644 --- a/src/app/create/CreateForm.tsx +++ b/src/app/create/CreateForm.tsx @@ -4,7 +4,7 @@ import Link from "next/link"; type Phase = "idle" | "submitting" | "generating" | "review" | "ready" | "failed"; -export default function CreateForm({ level: initialLevel, enabled }: { level: string; enabled: boolean }) { +export default function CreateForm({ level: initialLevel, levels, enabled }: { level: string; levels: { key: string; name: string }[]; enabled: boolean }) { const [level, setLevel] = useState(initialLevel); const [subject, setSubject] = useState(""); const [phase, setPhase] = useState("idle"); @@ -95,8 +95,9 @@ export default function CreateForm({ level: initialLevel, enabled }: { level: st