Unifies the new LangGraph-driven course-plan/media flow with robust provider fallbacks, admin AI provider settings, editable book-style materials, and strict entity isolation across LMS/course-plan APIs. Adds admin-only entity membership management in the Entities UI so users can switch linked entities directly from the platform. Made-with: Cursor
79 lines
2.3 KiB
TypeScript
79 lines
2.3 KiB
TypeScript
import { Badge } from "@/components/ui/badge";
|
|
import { cn } from "@/lib/utils";
|
|
import type { CoursePlanMaterial } from "@/types";
|
|
|
|
export const SKILL_STYLE: Record<string, string> = {
|
|
reading: "bg-blue-100 text-blue-800 border-blue-200",
|
|
writing: "bg-purple-100 text-purple-800 border-purple-200",
|
|
listening: "bg-emerald-100 text-emerald-800 border-emerald-200",
|
|
speaking: "bg-amber-100 text-amber-800 border-amber-200",
|
|
grammar: "bg-rose-100 text-rose-800 border-rose-200",
|
|
vocabulary: "bg-cyan-100 text-cyan-800 border-cyan-200",
|
|
integrated: "bg-slate-100 text-slate-800 border-slate-200",
|
|
};
|
|
|
|
export function SkillBadge({ skill }: { skill: string }) {
|
|
return (
|
|
<Badge
|
|
variant="outline"
|
|
className={cn("capitalize border", SKILL_STYLE[skill] ?? SKILL_STYLE.integrated)}
|
|
>
|
|
{skill || "integrated"}
|
|
</Badge>
|
|
);
|
|
}
|
|
|
|
function renderAny(value: unknown): React.ReactNode {
|
|
if (value == null) return null;
|
|
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
return <p className="leading-7">{String(value)}</p>;
|
|
}
|
|
if (Array.isArray(value)) {
|
|
if (value.length === 0) return null;
|
|
return (
|
|
<ul className="list-disc ps-5 space-y-1">
|
|
{value.map((item, idx) => (
|
|
<li key={idx}>{renderAny(item)}</li>
|
|
))}
|
|
</ul>
|
|
);
|
|
}
|
|
if (typeof value === "object") {
|
|
const entries = Object.entries(value as Record<string, unknown>);
|
|
return (
|
|
<div className="space-y-3">
|
|
{entries.map(([k, v]) => (
|
|
<div key={k}>
|
|
<h5 className="font-medium capitalize text-sm text-muted-foreground mb-1">
|
|
{k.replace(/_/g, " ")}
|
|
</h5>
|
|
<div className="text-sm">{renderAny(v)}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export default function MaterialBookView({
|
|
material,
|
|
}: {
|
|
material: Pick<CoursePlanMaterial, "body" | "body_text" | "material_type">;
|
|
}) {
|
|
const body = material.body ?? {};
|
|
const hasStructured = Object.keys(body).length > 0;
|
|
|
|
return (
|
|
<div className="rounded-lg border bg-background/70 p-4 space-y-4">
|
|
{hasStructured ? (
|
|
renderAny(body)
|
|
) : (
|
|
<p className="text-sm whitespace-pre-wrap leading-7">
|
|
{material.body_text || "No content available yet."}
|
|
</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|