@burdenoff/microfe-buildmyiq 2026.730.2 → 2026.731.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/buildmyiq/BuildmyiqRoot.d.ts.map +1 -1
- package/dist/buildmyiq/BuildmyiqRoot.js +68 -52
- package/dist/buildmyiq/BuildmyiqRoot.js.map +1 -1
- package/dist/buildmyiq/BuildmyiqRoutes.d.ts.map +1 -1
- package/dist/buildmyiq/BuildmyiqRoutes.js +36 -24
- package/dist/buildmyiq/BuildmyiqRoutes.js.map +1 -1
- package/dist/buildmyiq/hooks/useBuiPermissions.d.ts +1 -1
- package/dist/buildmyiq/hooks/useBuiPermissions.d.ts.map +1 -1
- package/dist/buildmyiq/hooks/useBuildmyiqApi.d.ts +38 -1
- package/dist/buildmyiq/hooks/useBuildmyiqApi.d.ts.map +1 -1
- package/dist/buildmyiq/hooks/useBuildmyiqApi.js +131 -86
- package/dist/buildmyiq/hooks/useBuildmyiqApi.js.map +1 -1
- package/dist/buildmyiq/pages/authoring/AuthoringPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/authoring/AuthoringPage.js +351 -124
- package/dist/buildmyiq/pages/authoring/AuthoringPage.js.map +1 -1
- package/dist/buildmyiq/pages/copilot/AdminCopilotPage.d.ts +4 -0
- package/dist/buildmyiq/pages/copilot/AdminCopilotPage.d.ts.map +1 -0
- package/dist/buildmyiq/pages/copilot/AdminCopilotPage.js +164 -0
- package/dist/buildmyiq/pages/copilot/AdminCopilotPage.js.map +1 -0
- package/dist/buildmyiq/pages/integrations/LtiPlatformsPage.d.ts +4 -0
- package/dist/buildmyiq/pages/integrations/LtiPlatformsPage.d.ts.map +1 -0
- package/dist/buildmyiq/pages/integrations/LtiPlatformsPage.js +269 -0
- package/dist/buildmyiq/pages/integrations/LtiPlatformsPage.js.map +1 -0
- package/dist/buildmyiq/pages/submissions/BulkGradingPage.d.ts +4 -0
- package/dist/buildmyiq/pages/submissions/BulkGradingPage.d.ts.map +1 -0
- package/dist/buildmyiq/pages/submissions/BulkGradingPage.js +229 -0
- package/dist/buildmyiq/pages/submissions/BulkGradingPage.js.map +1 -0
- package/dist/buildmyiq/pages/submissions/SubmissionsPage.d.ts.map +1 -1
- package/dist/buildmyiq/pages/submissions/SubmissionsPage.js +294 -177
- package/dist/buildmyiq/pages/submissions/SubmissionsPage.js.map +1 -1
- package/dist/buildmyiq/types/index.d.ts +95 -0
- package/dist/buildmyiq/types/index.d.ts.map +1 -1
- package/dist/buildmyiq/types/index.js.map +1 -1
- package/package.json +4 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AuthoringPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/authoring/AuthoringPage.tsx"],"sourcesContent":["/**\n * AuthoringPage — the instructor course-management surface.\n * @module buildmyiq/pages/authoring\n *\n * Lists EVERY workspace course (including DRAFT / IN_REVIEW) via `useBuiCourses`,\n * filterable by workflow status and optionally narrowed to the caller's own\n * courses. A \"New course\" modal creates a draft (`useBuiCreateCourse`) and jumps\n * straight into the CourseBuilder. All data is live `bui*` data — no fabrication.\n */\n\nimport type { FC } from 'react';\nimport { useMemo, useState } from 'react';\nimport { Plus } from 'lucide-react';\nimport { Button, GlassCard, Input, Switch, Textarea } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n FilterTabs,\n Pagination,\n EmptyState,\n LoadingView,\n ErrorView,\n} from '../../components';\nimport { CourseCard } from '../_shared';\nimport { Modal, Field, SelectField, NO_SKILL, slugify } from './_forms';\nimport { useBuildmyiq } from '../../context';\nimport { useBuiCourses, useBuiCreateCourse, useBuiSkills, useCursorPager } from '../../hooks';\nimport { COURSE_LEVELS } from '../../types';\nimport type { BuiCourse, BuiCourseLevel, BuiCourseStatus, BuiCreateCourseInput } from '../../types';\nimport { formatEnumLabel } from '../../utils/formatters';\nimport { BuiPermissionButton } from '../../components/BuiPermissionButton';\n\ntype StatusFilter = 'ALL' | BuiCourseStatus;\n\nconst STATUS_TABS: { value: StatusFilter; label: string }[] = [\n { value: 'ALL', label: 'All' },\n { value: 'DRAFT', label: 'Draft' },\n { value: 'IN_REVIEW', label: 'In review' },\n { value: 'PUBLISHED', label: 'Published' },\n { value: 'ARCHIVED', label: 'Archived' },\n];\n\nconst PAGE_SIZE = 24;\n\n/**\n * Create-course modal. Mounted only while open so its form state resets each\n * time it is opened.\n */\nconst NewCourseModal: FC<{ onClose: () => void; onCreated: (course: BuiCourse) => void }> = ({\n onClose,\n onCreated,\n}) => {\n const { currentUser } = useBuildmyiq();\n const skillsQuery = useBuiSkills();\n const createCourse = useBuiCreateCourse();\n\n const [title, setTitle] = useState('');\n const [slug, setSlug] = useState('');\n const [slugEdited, setSlugEdited] = useState(false);\n const [summary, setSummary] = useState('');\n const [level, setLevel] = useState<BuiCourseLevel>('BEGINNER');\n const [language, setLanguage] = useState('en');\n const [estimatedHours, setEstimatedHours] = useState('');\n const [skillId, setSkillId] = useState<string>(NO_SKILL);\n const [error, setError] = useState<string | null>(null);\n\n const skills = skillsQuery.data ?? [];\n\n const onTitleChange = (value: string) => {\n setTitle(value);\n if (!slugEdited) setSlug(slugify(value));\n };\n\n const canSubmit = title.trim().length > 0 && slug.trim().length > 0 && !createCourse.isPending;\n\n const handleSubmit = async () => {\n if (!canSubmit) return;\n setError(null);\n const input: BuiCreateCourseInput = {\n title: title.trim(),\n slug: slug.trim(),\n summary: summary.trim() || undefined,\n level,\n language: language.trim() || 'en',\n estimatedHours: estimatedHours.trim() ? Number(estimatedHours) : undefined,\n skillId: skillId !== NO_SKILL ? skillId : undefined,\n instructorId: currentUser?.id,\n };\n try {\n const course = await createCourse.mutateAsync(input);\n onCreated(course);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'Failed to create course.');\n }\n };\n\n return (\n <Modal\n open\n onClose={onClose}\n title=\"New course\"\n description=\"Create a draft course — you can add modules, lessons and assessments next.\"\n className=\"sm:max-w-xl\"\n footer={\n <>\n <Button variant=\"ghost\" onClick={onClose} disabled={createCourse.isPending}>\n Cancel\n </Button>\n <Button onClick={handleSubmit} disabled={!canSubmit}>\n {createCourse.isPending ? 'Creating…' : 'Create course'}\n </Button>\n </>\n }\n >\n <div className=\"grid gap-4\">\n <Field label=\"Title\" htmlFor=\"new-course-title\" required>\n <Input\n id=\"new-course-title\"\n value={title}\n onChange={(e) => onTitleChange(e.target.value)}\n placeholder=\"e.g. Foundations of Data Literacy\"\n />\n </Field>\n <Field\n label=\"Slug\"\n htmlFor=\"new-course-slug\"\n required\n hint=\"Used in the course URL. Auto-generated from the title — edit if you like.\"\n >\n <Input\n id=\"new-course-slug\"\n value={slug}\n onChange={(e) => {\n setSlug(e.target.value);\n setSlugEdited(true);\n }}\n placeholder=\"foundations-of-data-literacy\"\n />\n </Field>\n <Field label=\"Summary\" htmlFor=\"new-course-summary\">\n <Textarea\n id=\"new-course-summary\"\n rows={3}\n value={summary}\n onChange={(e) => setSummary(e.target.value)}\n placeholder=\"A short description shown on the catalog card.\"\n />\n </Field>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <Field label=\"Level\" htmlFor=\"new-course-level\">\n <SelectField\n id=\"new-course-level\"\n value={level}\n onChange={(v) => setLevel(v as BuiCourseLevel)}\n options={COURSE_LEVELS.map((l) => ({ value: l, label: formatEnumLabel(l) }))}\n />\n </Field>\n <Field label=\"Language\" htmlFor=\"new-course-language\">\n <Input\n id=\"new-course-language\"\n value={language}\n onChange={(e) => setLanguage(e.target.value)}\n placeholder=\"en\"\n />\n </Field>\n <Field label=\"Estimated hours\" htmlFor=\"new-course-hours\">\n <Input\n id=\"new-course-hours\"\n type=\"number\"\n min={0}\n value={estimatedHours}\n onChange={(e) => setEstimatedHours(e.target.value)}\n placeholder=\"0\"\n />\n </Field>\n <Field label=\"Skill\" htmlFor=\"new-course-skill\">\n <SelectField\n id=\"new-course-skill\"\n value={skillId}\n onChange={setSkillId}\n options={[\n { value: NO_SKILL, label: skillsQuery.isLoading ? 'Loading…' : 'No skill' },\n ...skills.map((s) => ({ value: s.id, label: s.name })),\n ]}\n />\n </Field>\n </div>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n </Modal>\n );\n};\n\nexport const AuthoringPage: FC = () => {\n const { currentUser, navigateTo } = useBuildmyiq();\n const myId = currentUser?.id;\n const [status, setStatus] = useState<StatusFilter>('ALL');\n const [mineOnly, setMineOnly] = useState(false);\n const [showCreate, setShowCreate] = useState(false);\n const pager = useCursorPager();\n\n const coursesQuery = useBuiCourses({\n status: status === 'ALL' ? undefined : status,\n cursor: pager.cursor,\n limit: PAGE_SIZE,\n });\n\n const items: BuiCourse[] = useMemo(() => coursesQuery.data?.items ?? [], [coursesQuery.data]);\n const visible = useMemo(\n () => (mineOnly && myId ? items.filter((c) => c.instructorId === myId) : items),\n [items, mineOnly, myId]\n );\n\n const handleStatus = (next: StatusFilter) => {\n setStatus(next);\n pager.reset();\n };\n\n const emptyTitle = mineOnly\n ? 'No courses you author yet'\n : status === 'ALL'\n ? 'No courses yet'\n : `No ${formatEnumLabel(status).toLowerCase()} courses`;\n\n return (\n <PageLayout\n title=\"Authoring\"\n description=\"Create, curate and publish your workspace courses.\"\n titleGradient\n aurora\n actions={\n <BuiPermissionButton resource=\"buildmyiq-course\" action=\"create\" deniedBehavior=\"hide\">\n <Button onClick={() => setShowCreate(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New course\n </Button>\n </BuiPermissionButton>\n }\n >\n <div className=\"flex flex-col gap-5\">\n <GlassCard\n treatment=\"plain\"\n className=\"flex flex-col gap-3 p-3 sm:flex-row sm:items-center sm:justify-between\"\n >\n <div className=\"-mx-1 overflow-x-auto px-1\">\n <FilterTabs\n options={STATUS_TABS}\n value={status}\n onChange={handleStatus}\n className=\"border-0 bg-transparent p-0\"\n />\n </div>\n <label className=\"inline-flex shrink-0 cursor-pointer items-center gap-2 text-sm text-text-secondary\">\n <Switch checked={mineOnly} onCheckedChange={setMineOnly} disabled={!currentUser?.id} />\n Mine only\n </label>\n </GlassCard>\n\n {coursesQuery.isLoading ? (\n <LoadingView label=\"Loading courses…\" />\n ) : coursesQuery.isError ? (\n <ErrorView\n message=\"We couldn't load your courses.\"\n onRetry={() => coursesQuery.refetch()}\n />\n ) : (\n <>\n {visible.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title={emptyTitle}\n description={\n mineOnly\n ? 'None of the courses on this page are authored by you — clear \"Mine only\" or page through to find them.'\n : 'Create your first course to start building curriculum, assessments and cohorts.'\n }\n action={\n <BuiPermissionButton\n resource=\"buildmyiq-course\"\n action=\"create\"\n deniedBehavior=\"hide\"\n >\n <Button onClick={() => setShowCreate(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New course\n </Button>\n </BuiPermissionButton>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4\">\n {visible.map((course) => (\n <CourseCard\n key={course.id}\n course={course}\n showStatus\n onClick={() => navigateTo(`authoring/${course.id}`)}\n />\n ))}\n </div>\n )}\n {/* Pager stays visible even when mineOnly trims the page empty, so the\n user can advance to later pages to find their courses. */}\n <Pagination\n page={pager.page}\n pageSize={PAGE_SIZE}\n itemCount={visible.length}\n hasMore={coursesQuery.data?.pageInfo.hasMore ?? false}\n onPageChange={(p) => pager.onPageChange(p, coursesQuery.data?.pageInfo.cursor)}\n />\n </>\n )}\n </div>\n\n {showCreate && (\n <NewCourseModal\n onClose={() => setShowCreate(false)}\n onCreated={(course) => {\n setShowCreate(false);\n navigateTo(`authoring/${course.id}`);\n }}\n />\n )}\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AuthoringPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/authoring/AuthoringPage.tsx"],"sourcesContent":["/**\n * AuthoringPage — the instructor course-management surface.\n * @module buildmyiq/pages/authoring\n *\n * Lists EVERY workspace course (including DRAFT / IN_REVIEW) via `useBuiCourses`,\n * filterable by workflow status and optionally narrowed to the caller's own\n * courses. A \"New course\" modal creates a draft (`useBuiCreateCourse`) and jumps\n * straight into the CourseBuilder. All data is live `bui*` data — no fabrication.\n */\n\nimport type { FC } from 'react';\nimport { useMemo, useState } from 'react';\nimport { Plus, Sparkles, Trash2 } from 'lucide-react';\nimport { Button, GlassCard, Input, Switch, Textarea } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n FilterTabs,\n Pagination,\n EmptyState,\n LoadingView,\n ErrorView,\n} from '../../components';\nimport { CourseCard } from '../_shared';\nimport { Modal, Field, SelectField, NO_SKILL, slugify } from './_forms';\nimport { useBuildmyiq } from '../../context';\nimport {\n useBuiCourses,\n useBuiCreateCourse,\n useBuiSkills,\n useBuiSuggestCourseOutline,\n useBuiApplyCourseOutline,\n useCursorPager,\n} from '../../hooks';\nimport { COURSE_LEVELS } from '../../types';\nimport type {\n BuiCourse,\n BuiCourseLevel,\n BuiCourseStatus,\n BuiCreateCourseInput,\n BuiCourseOutlineModule,\n BuiApplyCourseOutlineInput,\n} from '../../types';\nimport { formatEnumLabel } from '../../utils/formatters';\nimport { BuiPermissionButton } from '../../components/BuiPermissionButton';\n\ntype StatusFilter = 'ALL' | BuiCourseStatus;\n\nconst STATUS_TABS: { value: StatusFilter; label: string }[] = [\n { value: 'ALL', label: 'All' },\n { value: 'DRAFT', label: 'Draft' },\n { value: 'IN_REVIEW', label: 'In review' },\n { value: 'PUBLISHED', label: 'Published' },\n { value: 'ARCHIVED', label: 'Archived' },\n];\n\nconst PAGE_SIZE = 24;\n\n/**\n * Create-course modal. Mounted only while open so its form state resets each\n * time it is opened.\n */\nconst NewCourseModal: FC<{ onClose: () => void; onCreated: (course: BuiCourse) => void }> = ({\n onClose,\n onCreated,\n}) => {\n const { currentUser } = useBuildmyiq();\n const skillsQuery = useBuiSkills();\n const createCourse = useBuiCreateCourse();\n\n const [title, setTitle] = useState('');\n const [slug, setSlug] = useState('');\n const [slugEdited, setSlugEdited] = useState(false);\n const [summary, setSummary] = useState('');\n const [level, setLevel] = useState<BuiCourseLevel>('BEGINNER');\n const [language, setLanguage] = useState('en');\n const [estimatedHours, setEstimatedHours] = useState('');\n const [skillId, setSkillId] = useState<string>(NO_SKILL);\n const [error, setError] = useState<string | null>(null);\n\n const skills = skillsQuery.data ?? [];\n\n const onTitleChange = (value: string) => {\n setTitle(value);\n if (!slugEdited) setSlug(slugify(value));\n };\n\n const canSubmit = title.trim().length > 0 && slug.trim().length > 0 && !createCourse.isPending;\n\n const handleSubmit = async () => {\n if (!canSubmit) return;\n setError(null);\n const input: BuiCreateCourseInput = {\n title: title.trim(),\n slug: slug.trim(),\n summary: summary.trim() || undefined,\n level,\n language: language.trim() || 'en',\n estimatedHours: estimatedHours.trim() ? Number(estimatedHours) : undefined,\n skillId: skillId !== NO_SKILL ? skillId : undefined,\n instructorId: currentUser?.id,\n };\n try {\n const course = await createCourse.mutateAsync(input);\n onCreated(course);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'Failed to create course.');\n }\n };\n\n return (\n <Modal\n open\n onClose={onClose}\n title=\"New course\"\n description=\"Create a draft course — you can add modules, lessons and assessments next.\"\n className=\"sm:max-w-xl\"\n footer={\n <>\n <Button variant=\"ghost\" onClick={onClose} disabled={createCourse.isPending}>\n Cancel\n </Button>\n <Button onClick={handleSubmit} disabled={!canSubmit}>\n {createCourse.isPending ? 'Creating…' : 'Create course'}\n </Button>\n </>\n }\n >\n <div className=\"grid gap-4\">\n <Field label=\"Title\" htmlFor=\"new-course-title\" required>\n <Input\n id=\"new-course-title\"\n value={title}\n onChange={(e) => onTitleChange(e.target.value)}\n placeholder=\"e.g. Foundations of Data Literacy\"\n />\n </Field>\n <Field\n label=\"Slug\"\n htmlFor=\"new-course-slug\"\n required\n hint=\"Used in the course URL. Auto-generated from the title — edit if you like.\"\n >\n <Input\n id=\"new-course-slug\"\n value={slug}\n onChange={(e) => {\n setSlug(e.target.value);\n setSlugEdited(true);\n }}\n placeholder=\"foundations-of-data-literacy\"\n />\n </Field>\n <Field label=\"Summary\" htmlFor=\"new-course-summary\">\n <Textarea\n id=\"new-course-summary\"\n rows={3}\n value={summary}\n onChange={(e) => setSummary(e.target.value)}\n placeholder=\"A short description shown on the catalog card.\"\n />\n </Field>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <Field label=\"Level\" htmlFor=\"new-course-level\">\n <SelectField\n id=\"new-course-level\"\n value={level}\n onChange={(v) => setLevel(v as BuiCourseLevel)}\n options={COURSE_LEVELS.map((l) => ({ value: l, label: formatEnumLabel(l) }))}\n />\n </Field>\n <Field label=\"Language\" htmlFor=\"new-course-language\">\n <Input\n id=\"new-course-language\"\n value={language}\n onChange={(e) => setLanguage(e.target.value)}\n placeholder=\"en\"\n />\n </Field>\n <Field label=\"Estimated hours\" htmlFor=\"new-course-hours\">\n <Input\n id=\"new-course-hours\"\n type=\"number\"\n min={0}\n value={estimatedHours}\n onChange={(e) => setEstimatedHours(e.target.value)}\n placeholder=\"0\"\n />\n </Field>\n <Field label=\"Skill\" htmlFor=\"new-course-skill\">\n <SelectField\n id=\"new-course-skill\"\n value={skillId}\n onChange={setSkillId}\n options={[\n { value: NO_SKILL, label: skillsQuery.isLoading ? 'Loading…' : 'No skill' },\n ...skills.map((s) => ({ value: s.id, label: s.name })),\n ]}\n />\n </Field>\n </div>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n </Modal>\n );\n};\n\n/**\n * AI Course Builder (BOFF-4434). Two steps: (1) describe a topic and get an\n * AI-drafted outline (`buiSuggestCourseOutline` — an EPHEMERAL suggestion,\n * never persisted server-side); (2) review/edit the outline (drop modules or\n * lessons that don't fit) and create a real DRAFT course from it\n * (`buiApplyCourseOutline`). The resulting course is always DRAFT — nothing\n * AI-drafted ever reaches learners without an explicit `buiPublishCourse`.\n */\nconst AiCourseBuilderModal: FC<{ onClose: () => void; onCreated: (course: BuiCourse) => void }> = ({\n onClose,\n onCreated,\n}) => {\n const suggestOutline = useBuiSuggestCourseOutline();\n const applyOutline = useBuiApplyCourseOutline();\n\n const [topic, setTopic] = useState('');\n const [audienceLevel, setAudienceLevel] = useState('');\n const [moduleCount, setModuleCount] = useState('4');\n const [genError, setGenError] = useState<string | null>(null);\n\n const [rationale, setRationale] = useState<string | null>(null);\n const [modules, setModules] = useState<BuiCourseOutlineModule[] | null>(null);\n const [title, setTitle] = useState('');\n const [slug, setSlug] = useState('');\n const [applyError, setApplyError] = useState<string | null>(null);\n\n const handleGenerate = async () => {\n if (!topic.trim() || suggestOutline.isPending) return;\n setGenError(null);\n try {\n const result = await suggestOutline.mutateAsync({\n topic: topic.trim(),\n audienceLevel: audienceLevel.trim() || null,\n moduleCount: moduleCount.trim() ? Number(moduleCount) : null,\n });\n setRationale(result.rationale);\n setModules(result.modules);\n setTitle(topic.trim());\n setSlug(slugify(topic.trim()));\n } catch (e) {\n setGenError(\n e instanceof Error ? e.message : 'The AI course builder is unavailable right now.'\n );\n }\n };\n\n const removeModule = (idx: number) => {\n setModules((prev) => (prev ? prev.filter((_, i) => i !== idx) : prev));\n };\n\n const removeLesson = (moduleIdx: number, lessonIdx: number) => {\n setModules((prev) =>\n prev\n ? prev.map((m, i) =>\n i === moduleIdx ? { ...m, lessons: m.lessons.filter((_, li) => li !== lessonIdx) } : m\n )\n : prev\n );\n };\n\n const canApply =\n !!modules &&\n modules.length > 0 &&\n title.trim().length > 0 &&\n slug.trim().length > 0 &&\n !applyOutline.isPending;\n\n const handleApply = async () => {\n if (!canApply || !modules) return;\n setApplyError(null);\n const input: BuiApplyCourseOutlineInput = {\n title: title.trim(),\n slug: slug.trim(),\n modules: modules.map((m) => ({\n title: m.title,\n description: m.description ?? undefined,\n lessons: m.lessons.map((l) => ({\n title: l.title,\n description: l.description ?? undefined,\n })),\n })),\n };\n try {\n const course = await applyOutline.mutateAsync(input);\n onCreated(course);\n } catch (e) {\n setApplyError(\n e instanceof Error ? e.message : 'Failed to create the course from this outline.'\n );\n }\n };\n\n return (\n <Modal\n open\n onClose={onClose}\n title=\"Course builder — suggest with AI\"\n description=\"Describe a topic and get a draft module/lesson outline to review and edit before it becomes a real course.\"\n className=\"sm:max-w-2xl\"\n footer={\n modules ? (\n <>\n <Button variant=\"ghost\" onClick={onClose} disabled={applyOutline.isPending}>\n Cancel\n </Button>\n <Button onClick={handleApply} disabled={!canApply}>\n {applyOutline.isPending ? 'Creating…' : 'Create draft course'}\n </Button>\n </>\n ) : (\n <>\n <Button variant=\"ghost\" onClick={onClose} disabled={suggestOutline.isPending}>\n Cancel\n </Button>\n <Button onClick={handleGenerate} disabled={!topic.trim() || suggestOutline.isPending}>\n <Sparkles className=\"mr-1.5 h-4 w-4\" />\n {suggestOutline.isPending ? 'Drafting outline…' : 'Generate outline'}\n </Button>\n </>\n )\n }\n >\n {!modules ? (\n <div className=\"grid gap-4\">\n <Field\n label=\"Topic\"\n htmlFor=\"outline-topic\"\n required\n hint=\"What should the course teach?\"\n >\n <Textarea\n id=\"outline-topic\"\n rows={2}\n value={topic}\n onChange={(e) => setTopic(e.target.value)}\n placeholder=\"e.g. Introduction to SQL for data analysts\"\n />\n </Field>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <Field label=\"Audience level (optional)\" htmlFor=\"outline-audience\">\n <Input\n id=\"outline-audience\"\n value={audienceLevel}\n onChange={(e) => setAudienceLevel(e.target.value)}\n placeholder=\"e.g. beginners with no prior SQL\"\n />\n </Field>\n <Field label=\"Target module count\" htmlFor=\"outline-modules\">\n <Input\n id=\"outline-modules\"\n type=\"number\"\n min={1}\n max={10}\n value={moduleCount}\n onChange={(e) => setModuleCount(e.target.value)}\n />\n </Field>\n </div>\n {genError && <p className=\"text-sm text-status-error-text\">{genError}</p>}\n </div>\n ) : (\n <div className=\"grid gap-4\">\n {rationale && <p className=\"text-sm text-text-secondary\">{rationale}</p>}\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <Field label=\"Course title\" htmlFor=\"outline-title\" required>\n <Input id=\"outline-title\" value={title} onChange={(e) => setTitle(e.target.value)} />\n </Field>\n <Field label=\"Slug\" htmlFor=\"outline-slug\" required>\n <Input id=\"outline-slug\" value={slug} onChange={(e) => setSlug(e.target.value)} />\n </Field>\n </div>\n <div className=\"flex flex-col gap-3\">\n {modules.map((m, mi) => (\n <GlassCard key={`${m.title}-${mi}`} treatment=\"plain\" className=\"p-3\">\n <div className=\"flex items-start justify-between gap-2\">\n <div className=\"min-w-0\">\n <p className=\"font-medium text-text-primary\">\n Module {mi + 1} · {m.title}\n </p>\n {m.description && (\n <p className=\"text-xs text-text-secondary\">{m.description}</p>\n )}\n </div>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => removeModule(mi)}\n aria-label={`Remove module ${m.title}`}\n >\n <Trash2 className=\"h-4 w-4\" />\n </Button>\n </div>\n <ul className=\"mt-2 flex flex-col gap-1 pl-3\">\n {m.lessons.map((l, li) => (\n <li\n key={`${l.title}-${li}`}\n className=\"flex items-center justify-between gap-2 text-sm\"\n >\n <span className=\"min-w-0 truncate text-text-secondary\">\n <span className=\"text-text-tertiary\">\n {mi + 1}.{li + 1}\n </span>{' '}\n {l.title}\n </span>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => removeLesson(mi, li)}\n aria-label={`Remove lesson ${l.title}`}\n >\n <Trash2 className=\"h-3.5 w-3.5\" />\n </Button>\n </li>\n ))}\n </ul>\n </GlassCard>\n ))}\n {modules.length === 0 && (\n <p className=\"text-sm text-text-secondary\">\n Every module was removed — go back and generate again, or cancel.\n </p>\n )}\n </div>\n {applyError && <p className=\"text-sm text-status-error-text\">{applyError}</p>}\n </div>\n )}\n </Modal>\n );\n};\n\nexport const AuthoringPage: FC = () => {\n const { currentUser, navigateTo } = useBuildmyiq();\n const myId = currentUser?.id;\n const [status, setStatus] = useState<StatusFilter>('ALL');\n const [mineOnly, setMineOnly] = useState(false);\n const [showCreate, setShowCreate] = useState(false);\n const [showAiBuilder, setShowAiBuilder] = useState(false);\n const pager = useCursorPager();\n\n const coursesQuery = useBuiCourses({\n status: status === 'ALL' ? undefined : status,\n cursor: pager.cursor,\n limit: PAGE_SIZE,\n });\n\n const items: BuiCourse[] = useMemo(() => coursesQuery.data?.items ?? [], [coursesQuery.data]);\n const visible = useMemo(\n () => (mineOnly && myId ? items.filter((c) => c.instructorId === myId) : items),\n [items, mineOnly, myId]\n );\n\n const handleStatus = (next: StatusFilter) => {\n setStatus(next);\n pager.reset();\n };\n\n const emptyTitle = mineOnly\n ? 'No courses you author yet'\n : status === 'ALL'\n ? 'No courses yet'\n : `No ${formatEnumLabel(status).toLowerCase()} courses`;\n\n return (\n <PageLayout\n title=\"Authoring\"\n description=\"Create, curate and publish your workspace courses.\"\n titleGradient\n aurora\n actions={\n <BuiPermissionButton resource=\"buildmyiq-course\" action=\"create\" deniedBehavior=\"hide\">\n <div className=\"flex gap-2\">\n <Button variant=\"outline\" onClick={() => setShowAiBuilder(true)}>\n <Sparkles className=\"mr-1.5 h-4 w-4\" /> Suggest with AI\n </Button>\n <Button onClick={() => setShowCreate(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New course\n </Button>\n </div>\n </BuiPermissionButton>\n }\n >\n <div className=\"flex flex-col gap-5\">\n <GlassCard\n treatment=\"plain\"\n className=\"flex flex-col gap-3 p-3 sm:flex-row sm:items-center sm:justify-between\"\n >\n <div className=\"-mx-1 overflow-x-auto px-1\">\n <FilterTabs\n options={STATUS_TABS}\n value={status}\n onChange={handleStatus}\n className=\"border-0 bg-transparent p-0\"\n />\n </div>\n <label className=\"inline-flex shrink-0 cursor-pointer items-center gap-2 text-sm text-text-secondary\">\n <Switch checked={mineOnly} onCheckedChange={setMineOnly} disabled={!currentUser?.id} />\n Mine only\n </label>\n </GlassCard>\n\n {coursesQuery.isLoading ? (\n <LoadingView label=\"Loading courses…\" />\n ) : coursesQuery.isError ? (\n <ErrorView\n message=\"We couldn't load your courses.\"\n onRetry={() => coursesQuery.refetch()}\n />\n ) : (\n <>\n {visible.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title={emptyTitle}\n description={\n mineOnly\n ? 'None of the courses on this page are authored by you — clear \"Mine only\" or page through to find them.'\n : 'Create your first course to start building curriculum, assessments and cohorts.'\n }\n action={\n <BuiPermissionButton\n resource=\"buildmyiq-course\"\n action=\"create\"\n deniedBehavior=\"hide\"\n >\n <Button onClick={() => setShowCreate(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> New course\n </Button>\n </BuiPermissionButton>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 2xl:grid-cols-4\">\n {visible.map((course) => (\n <CourseCard\n key={course.id}\n course={course}\n showStatus\n onClick={() => navigateTo(`authoring/${course.id}`)}\n />\n ))}\n </div>\n )}\n {/* Pager stays visible even when mineOnly trims the page empty, so the\n user can advance to later pages to find their courses. */}\n <Pagination\n page={pager.page}\n pageSize={PAGE_SIZE}\n itemCount={visible.length}\n hasMore={coursesQuery.data?.pageInfo.hasMore ?? false}\n onPageChange={(p) => pager.onPageChange(p, coursesQuery.data?.pageInfo.cursor)}\n />\n </>\n )}\n </div>\n\n {showCreate && (\n <NewCourseModal\n onClose={() => setShowCreate(false)}\n onCreated={(course) => {\n setShowCreate(false);\n navigateTo(`authoring/${course.id}`);\n }}\n />\n )}\n {showAiBuilder && (\n <AiCourseBuilderModal\n onClose={() => setShowAiBuilder(false)}\n onCreated={(course) => {\n setShowAiBuilder(false);\n navigateTo(`authoring/${course.id}`);\n }}\n />\n )}\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AdminCopilotPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/copilot/AdminCopilotPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AA0BhC,eAAO,MAAM,gBAAgB,EAAE,EAkJ9B,CAAC"}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { useBuildmyiq as e } from "../../context/BuildmyiqContext.js";
|
|
2
|
+
import "../../context/index.js";
|
|
3
|
+
import { PageLayout as t } from "../../components/PageLayout.js";
|
|
4
|
+
import { useBuiAdminCopilotAsk as n } from "../../hooks/useBuildmyiqApi.js";
|
|
5
|
+
import "../../hooks/index.js";
|
|
6
|
+
import "../../components/index.js";
|
|
7
|
+
import { useEffect as r, useRef as i, useState as a } from "react";
|
|
8
|
+
import { Bot as o, Send as s, ShieldQuestion as c, Sparkles as l } from "lucide-react";
|
|
9
|
+
import { jsx as u, jsxs as d } from "react/jsx-runtime";
|
|
10
|
+
import { Button as f, GlassCard as p, Textarea as m } from "@burdenoff/fe-libs/ui";
|
|
11
|
+
//#region src/buildmyiq/pages/copilot/AdminCopilotPage.tsx
|
|
12
|
+
var h = [
|
|
13
|
+
"How many published courses do we have?",
|
|
14
|
+
"What is our overall course completion rate?",
|
|
15
|
+
"Which courses have the most graded submissions?"
|
|
16
|
+
];
|
|
17
|
+
function g(e) {
|
|
18
|
+
if (!e) return "You";
|
|
19
|
+
let t = [e.firstName, e.lastName].filter(Boolean).join(" ").trim();
|
|
20
|
+
return e.name || t || e.username || e.email || "You";
|
|
21
|
+
}
|
|
22
|
+
var _ = () => {
|
|
23
|
+
let { currentUser: _ } = e(), v = n(), [y, b] = a([]), [x, S] = a(""), [C, w] = a(null), T = i(null), E = g(_);
|
|
24
|
+
r(() => {
|
|
25
|
+
T.current?.scrollTo({
|
|
26
|
+
top: T.current.scrollHeight,
|
|
27
|
+
behavior: "smooth"
|
|
28
|
+
});
|
|
29
|
+
}, [y.length]);
|
|
30
|
+
let D = async (e) => {
|
|
31
|
+
let t = e.trim();
|
|
32
|
+
if (!t || v.isPending) return;
|
|
33
|
+
w(null);
|
|
34
|
+
let n = y.map((e) => ({
|
|
35
|
+
role: e.role,
|
|
36
|
+
content: e.content
|
|
37
|
+
}));
|
|
38
|
+
b((e) => [...e, {
|
|
39
|
+
role: "USER",
|
|
40
|
+
content: t
|
|
41
|
+
}]), S("");
|
|
42
|
+
try {
|
|
43
|
+
let e = await v.mutateAsync({
|
|
44
|
+
question: t,
|
|
45
|
+
history: n
|
|
46
|
+
});
|
|
47
|
+
b((t) => [...t, {
|
|
48
|
+
role: "ASSISTANT",
|
|
49
|
+
content: e.answer
|
|
50
|
+
}]);
|
|
51
|
+
} catch (e) {
|
|
52
|
+
w(e instanceof Error ? e.message : "The admin copilot is temporarily unavailable."), b((e) => e.slice(0, -1)), S(t);
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
return /* @__PURE__ */ u(t, {
|
|
56
|
+
title: "Admin copilot",
|
|
57
|
+
description: "Ask questions about your workspace's courses, enrollments and grading — read-only, grounded in real data.",
|
|
58
|
+
titleGradient: !0,
|
|
59
|
+
children: /* @__PURE__ */ d(p, {
|
|
60
|
+
treatment: "plain",
|
|
61
|
+
className: "flex flex-col overflow-hidden p-0",
|
|
62
|
+
children: [
|
|
63
|
+
/* @__PURE__ */ d("div", {
|
|
64
|
+
className: "flex items-center gap-2 border-b border-border-seam px-4 py-3",
|
|
65
|
+
children: [/* @__PURE__ */ u("span", {
|
|
66
|
+
className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[var(--color-accent-soft)]",
|
|
67
|
+
children: /* @__PURE__ */ u(l, { className: "h-4 w-4 text-[var(--color-accent-teal)]" })
|
|
68
|
+
}), /* @__PURE__ */ d("div", {
|
|
69
|
+
className: "min-w-0",
|
|
70
|
+
children: [/* @__PURE__ */ u("p", {
|
|
71
|
+
className: "text-sm font-semibold text-text-primary",
|
|
72
|
+
children: "BuildMyIQ Admin Copilot"
|
|
73
|
+
}), /* @__PURE__ */ u("p", {
|
|
74
|
+
className: "text-xs text-text-secondary",
|
|
75
|
+
children: "Answers only from live workspace statistics — never executes an action."
|
|
76
|
+
})]
|
|
77
|
+
})]
|
|
78
|
+
}),
|
|
79
|
+
/* @__PURE__ */ u("div", {
|
|
80
|
+
ref: T,
|
|
81
|
+
className: "max-h-[28rem] min-h-64 flex-1 overflow-y-auto px-4 py-4",
|
|
82
|
+
children: y.length === 0 ? /* @__PURE__ */ d("div", {
|
|
83
|
+
className: "flex flex-col items-center gap-3 py-8 text-center",
|
|
84
|
+
children: [
|
|
85
|
+
/* @__PURE__ */ u(c, { className: "h-9 w-9 text-text-tertiary" }),
|
|
86
|
+
/* @__PURE__ */ u("p", {
|
|
87
|
+
className: "max-w-sm text-sm text-text-secondary",
|
|
88
|
+
children: "Ask about course counts, enrollment/completion rates, or grading volume for this workspace."
|
|
89
|
+
}),
|
|
90
|
+
/* @__PURE__ */ u("div", {
|
|
91
|
+
className: "flex flex-wrap justify-center gap-2",
|
|
92
|
+
children: h.map((e) => /* @__PURE__ */ u(f, {
|
|
93
|
+
variant: "outline",
|
|
94
|
+
size: "sm",
|
|
95
|
+
onClick: () => D(e),
|
|
96
|
+
disabled: v.isPending,
|
|
97
|
+
children: e
|
|
98
|
+
}, e))
|
|
99
|
+
})
|
|
100
|
+
]
|
|
101
|
+
}) : /* @__PURE__ */ d("ul", {
|
|
102
|
+
className: "flex flex-col gap-4",
|
|
103
|
+
children: [y.map((e, t) => {
|
|
104
|
+
let n = e.role === "ASSISTANT";
|
|
105
|
+
return /* @__PURE__ */ d("li", {
|
|
106
|
+
className: "flex gap-3",
|
|
107
|
+
children: [/* @__PURE__ */ u("div", {
|
|
108
|
+
className: n ? "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[var(--color-accent-soft)]" : "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-sm font-semibold text-text-secondary",
|
|
109
|
+
children: n ? /* @__PURE__ */ u(o, { className: "h-4 w-4 text-[var(--color-accent-teal)]" }) : E.trim()[0]?.toUpperCase() ?? "?"
|
|
110
|
+
}), /* @__PURE__ */ d("div", {
|
|
111
|
+
className: "min-w-0 flex-1",
|
|
112
|
+
children: [/* @__PURE__ */ u("span", {
|
|
113
|
+
className: "text-sm font-semibold text-text-primary",
|
|
114
|
+
children: n ? "Admin Copilot" : E
|
|
115
|
+
}), /* @__PURE__ */ u("p", {
|
|
116
|
+
className: "mt-0.5 whitespace-pre-wrap break-words text-sm text-text-secondary",
|
|
117
|
+
children: e.content
|
|
118
|
+
})]
|
|
119
|
+
})]
|
|
120
|
+
}, t);
|
|
121
|
+
}), v.isPending && /* @__PURE__ */ d("li", {
|
|
122
|
+
className: "flex items-center gap-3",
|
|
123
|
+
children: [/* @__PURE__ */ u("span", {
|
|
124
|
+
className: "flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[var(--color-accent-soft)]",
|
|
125
|
+
children: /* @__PURE__ */ u(o, { className: "h-4 w-4 animate-pulse text-[var(--color-accent-teal)]" })
|
|
126
|
+
}), /* @__PURE__ */ u("span", {
|
|
127
|
+
className: "text-sm text-text-secondary",
|
|
128
|
+
children: "Checking workspace statistics…"
|
|
129
|
+
})]
|
|
130
|
+
})]
|
|
131
|
+
})
|
|
132
|
+
}),
|
|
133
|
+
/* @__PURE__ */ d("div", {
|
|
134
|
+
className: "border-t border-border-seam p-3",
|
|
135
|
+
children: [/* @__PURE__ */ u(m, {
|
|
136
|
+
value: x,
|
|
137
|
+
onChange: (e) => S(e.target.value),
|
|
138
|
+
onKeyDown: (e) => {
|
|
139
|
+
e.key === "Enter" && !e.shiftKey && (e.preventDefault(), D(x));
|
|
140
|
+
},
|
|
141
|
+
placeholder: "Ask about courses, enrollments, grading…",
|
|
142
|
+
rows: 2,
|
|
143
|
+
disabled: v.isPending
|
|
144
|
+
}), /* @__PURE__ */ d("div", {
|
|
145
|
+
className: "mt-2 flex items-center justify-end gap-2",
|
|
146
|
+
children: [C && /* @__PURE__ */ u("span", {
|
|
147
|
+
className: "mr-auto text-xs text-status-error-text",
|
|
148
|
+
children: C
|
|
149
|
+
}), /* @__PURE__ */ d(f, {
|
|
150
|
+
size: "sm",
|
|
151
|
+
onClick: () => D(x),
|
|
152
|
+
disabled: !x.trim() || v.isPending,
|
|
153
|
+
children: [/* @__PURE__ */ u(s, { className: "mr-1.5 h-4 w-4" }), v.isPending ? "Asking…" : "Ask"]
|
|
154
|
+
})]
|
|
155
|
+
})]
|
|
156
|
+
})
|
|
157
|
+
]
|
|
158
|
+
})
|
|
159
|
+
});
|
|
160
|
+
};
|
|
161
|
+
//#endregion
|
|
162
|
+
export { _ as AdminCopilotPage };
|
|
163
|
+
|
|
164
|
+
//# sourceMappingURL=AdminCopilotPage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"AdminCopilotPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/copilot/AdminCopilotPage.tsx"],"sourcesContent":["/**\n * AdminCopilotPage — read-only L&D admin Q&A chat (BOFF-4434).\n * @module buildmyiq/pages/copilot\n *\n * A workspace-operator surface: every answer comes from `buiAdminCopilotAsk`,\n * grounded in real, live-computed course/enrollment/assessment aggregate\n * stats — never an ungrounded completion, and it NEVER executes any admin\n * action (create/publish/delete a course, etc.) — it only answers questions.\n * Stateless server-side: this page holds the transcript itself and resends\n * prior turns as `history` on every ask for multi-turn context.\n */\n\nimport type { FC } from 'react';\nimport { useEffect, useRef, useState } from 'react';\nimport { Bot, Send, ShieldQuestion, Sparkles } from 'lucide-react';\nimport { Button, GlassCard, Textarea } from '@burdenoff/fe-libs/ui';\nimport { PageLayout } from '../../components';\nimport { useBuiAdminCopilotAsk } from '../../hooks';\nimport { useBuildmyiq } from '../../context';\nimport type { BuiAdminCopilotTurnInput } from '../../types';\n\ninterface ChatTurn {\n role: 'USER' | 'ASSISTANT';\n content: string;\n}\n\nconst SUGGESTED_QUESTIONS = [\n 'How many published courses do we have?',\n 'What is our overall course completion rate?',\n 'Which courses have the most graded submissions?',\n];\n\nfunction displayName(user: ReturnType<typeof useBuildmyiq>['currentUser']): string {\n if (!user) return 'You';\n const full = [user.firstName, user.lastName].filter(Boolean).join(' ').trim();\n return user.name || full || user.username || user.email || 'You';\n}\n\nexport const AdminCopilotPage: FC = () => {\n const { currentUser } = useBuildmyiq();\n const ask = useBuiAdminCopilotAsk();\n const [turns, setTurns] = useState<ChatTurn[]>([]);\n const [draft, setDraft] = useState('');\n const [error, setError] = useState<string | null>(null);\n const scrollRef = useRef<HTMLDivElement>(null);\n const selfName = displayName(currentUser);\n\n useEffect(() => {\n scrollRef.current?.scrollTo({ top: scrollRef.current.scrollHeight, behavior: 'smooth' });\n }, [turns.length]);\n\n const send = async (question: string) => {\n const trimmed = question.trim();\n if (!trimmed || ask.isPending) return;\n setError(null);\n const history: BuiAdminCopilotTurnInput[] = turns.map((t) => ({\n role: t.role,\n content: t.content,\n }));\n setTurns((prev) => [...prev, { role: 'USER', content: trimmed }]);\n setDraft('');\n try {\n const result = await ask.mutateAsync({ question: trimmed, history });\n setTurns((prev) => [...prev, { role: 'ASSISTANT', content: result.answer }]);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'The admin copilot is temporarily unavailable.');\n // Roll back the optimistic USER turn so a retry doesn't duplicate it.\n setTurns((prev) => prev.slice(0, -1));\n setDraft(trimmed);\n }\n };\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {\n if (e.key === 'Enter' && !e.shiftKey) {\n e.preventDefault();\n void send(draft);\n }\n };\n\n return (\n <PageLayout\n title=\"Admin copilot\"\n description=\"Ask questions about your workspace's courses, enrollments and grading — read-only, grounded in real data.\"\n titleGradient\n >\n <GlassCard treatment=\"plain\" className=\"flex flex-col overflow-hidden p-0\">\n <div className=\"flex items-center gap-2 border-b border-border-seam px-4 py-3\">\n <span className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[var(--color-accent-soft)]\">\n <Sparkles className=\"h-4 w-4 text-[var(--color-accent-teal)]\" />\n </span>\n <div className=\"min-w-0\">\n <p className=\"text-sm font-semibold text-text-primary\">BuildMyIQ Admin Copilot</p>\n <p className=\"text-xs text-text-secondary\">\n Answers only from live workspace statistics — never executes an action.\n </p>\n </div>\n </div>\n\n <div ref={scrollRef} className=\"max-h-[28rem] min-h-64 flex-1 overflow-y-auto px-4 py-4\">\n {turns.length === 0 ? (\n <div className=\"flex flex-col items-center gap-3 py-8 text-center\">\n <ShieldQuestion className=\"h-9 w-9 text-text-tertiary\" />\n <p className=\"max-w-sm text-sm text-text-secondary\">\n Ask about course counts, enrollment/completion rates, or grading volume for this\n workspace.\n </p>\n <div className=\"flex flex-wrap justify-center gap-2\">\n {SUGGESTED_QUESTIONS.map((q) => (\n <Button\n key={q}\n variant=\"outline\"\n size=\"sm\"\n onClick={() => send(q)}\n disabled={ask.isPending}\n >\n {q}\n </Button>\n ))}\n </div>\n </div>\n ) : (\n <ul className=\"flex flex-col gap-4\">\n {turns.map((turn, i) => {\n const isAssistant = turn.role === 'ASSISTANT';\n return (\n <li key={i} className=\"flex gap-3\">\n <div\n className={\n isAssistant\n ? 'flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[var(--color-accent-soft)]'\n : 'flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-bg-sunken text-sm font-semibold text-text-secondary'\n }\n >\n {isAssistant ? (\n <Bot className=\"h-4 w-4 text-[var(--color-accent-teal)]\" />\n ) : (\n (selfName.trim()[0]?.toUpperCase() ?? '?')\n )}\n </div>\n <div className=\"min-w-0 flex-1\">\n <span className=\"text-sm font-semibold text-text-primary\">\n {isAssistant ? 'Admin Copilot' : selfName}\n </span>\n <p className=\"mt-0.5 whitespace-pre-wrap break-words text-sm text-text-secondary\">\n {turn.content}\n </p>\n </div>\n </li>\n );\n })}\n {ask.isPending && (\n <li className=\"flex items-center gap-3\">\n <span className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[var(--color-accent-soft)]\">\n <Bot className=\"h-4 w-4 animate-pulse text-[var(--color-accent-teal)]\" />\n </span>\n <span className=\"text-sm text-text-secondary\">\n Checking workspace statistics…\n </span>\n </li>\n )}\n </ul>\n )}\n </div>\n\n <div className=\"border-t border-border-seam p-3\">\n <Textarea\n value={draft}\n onChange={(e) => setDraft(e.target.value)}\n onKeyDown={handleKeyDown}\n placeholder=\"Ask about courses, enrollments, grading…\"\n rows={2}\n disabled={ask.isPending}\n />\n <div className=\"mt-2 flex items-center justify-end gap-2\">\n {error && <span className=\"mr-auto text-xs text-status-error-text\">{error}</span>}\n <Button size=\"sm\" onClick={() => send(draft)} disabled={!draft.trim() || ask.isPending}>\n <Send className=\"mr-1.5 h-4 w-4\" />\n {ask.isPending ? 'Asking…' : 'Ask'}\n </Button>\n </div>\n </div>\n </GlassCard>\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LtiPlatformsPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/integrations/LtiPlatformsPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAiPhC,eAAO,MAAM,gBAAgB,EAAE,EA+D9B,CAAC"}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { PageLayout as e } from "../../components/PageLayout.js";
|
|
2
|
+
import { formatDate as t } from "../../utils/formatters.js";
|
|
3
|
+
import { EmptyState as n } from "../../components/EmptyState.js";
|
|
4
|
+
import { ErrorView as r, LoadingView as i } from "../../components/StateViews.js";
|
|
5
|
+
import { useBuiDeactivateLtiPlatform as a, useBuiLtiPlatforms as o, useBuiRegisterLtiPlatform as s } from "../../hooks/useBuildmyiqApi.js";
|
|
6
|
+
import "../../hooks/index.js";
|
|
7
|
+
import { BuiPermissionButton as c } from "../../components/BuiPermissionButton.js";
|
|
8
|
+
import "../../components/index.js";
|
|
9
|
+
import { Field as l, Modal as u } from "../authoring/_forms.js";
|
|
10
|
+
import { useState as d } from "react";
|
|
11
|
+
import { Plug as f, Plus as p, Power as m } from "lucide-react";
|
|
12
|
+
import { Fragment as h, jsx as g, jsxs as _ } from "react/jsx-runtime";
|
|
13
|
+
import { Button as v, GlassCard as y, Input as b } from "@burdenoff/fe-libs/ui";
|
|
14
|
+
//#region src/buildmyiq/pages/integrations/LtiPlatformsPage.tsx
|
|
15
|
+
var x = ({ onClose: e, onRegistered: t }) => {
|
|
16
|
+
let n = s(), [r, i] = d(""), [a, o] = d(""), [c, f] = d(""), [p, m] = d(""), [y, x] = d(""), [S, C] = d(""), [w, T] = d(""), [E, D] = d(null), O = r.trim() && a.trim() && c.trim() && y.trim() && S.trim() && w.trim() && !n.isPending;
|
|
17
|
+
return /* @__PURE__ */ g(u, {
|
|
18
|
+
open: !0,
|
|
19
|
+
onClose: e,
|
|
20
|
+
title: "Register LMS platform",
|
|
21
|
+
description: "Add the LTI 1.3 Advantage platform details from your LMS admin console (e.g. Canvas, Moodle, Brightspace).",
|
|
22
|
+
className: "sm:max-w-xl",
|
|
23
|
+
footer: /* @__PURE__ */ _(h, { children: [/* @__PURE__ */ g(v, {
|
|
24
|
+
variant: "ghost",
|
|
25
|
+
onClick: e,
|
|
26
|
+
disabled: n.isPending,
|
|
27
|
+
children: "Cancel"
|
|
28
|
+
}), /* @__PURE__ */ g(v, {
|
|
29
|
+
onClick: async () => {
|
|
30
|
+
if (!O) return;
|
|
31
|
+
D(null);
|
|
32
|
+
let e = {
|
|
33
|
+
name: r.trim(),
|
|
34
|
+
issuer: a.trim(),
|
|
35
|
+
clientId: c.trim(),
|
|
36
|
+
deploymentIds: p.split(",").map((e) => e.trim()).filter(Boolean),
|
|
37
|
+
authLoginUrl: y.trim(),
|
|
38
|
+
authTokenUrl: S.trim(),
|
|
39
|
+
keySetUrl: w.trim()
|
|
40
|
+
};
|
|
41
|
+
try {
|
|
42
|
+
await n.mutateAsync(e), t();
|
|
43
|
+
} catch (e) {
|
|
44
|
+
D(e instanceof Error ? e.message : "Failed to register the platform.");
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
disabled: !O,
|
|
48
|
+
children: n.isPending ? "Registering…" : "Register platform"
|
|
49
|
+
})] }),
|
|
50
|
+
children: /* @__PURE__ */ _("div", {
|
|
51
|
+
className: "grid gap-4",
|
|
52
|
+
children: [
|
|
53
|
+
/* @__PURE__ */ g(l, {
|
|
54
|
+
label: "Platform name",
|
|
55
|
+
htmlFor: "lti-name",
|
|
56
|
+
required: !0,
|
|
57
|
+
children: /* @__PURE__ */ g(b, {
|
|
58
|
+
id: "lti-name",
|
|
59
|
+
value: r,
|
|
60
|
+
onChange: (e) => i(e.target.value),
|
|
61
|
+
placeholder: "e.g. Acme University Canvas"
|
|
62
|
+
})
|
|
63
|
+
}),
|
|
64
|
+
/* @__PURE__ */ _("div", {
|
|
65
|
+
className: "grid gap-4 sm:grid-cols-2",
|
|
66
|
+
children: [/* @__PURE__ */ g(l, {
|
|
67
|
+
label: "Issuer",
|
|
68
|
+
htmlFor: "lti-issuer",
|
|
69
|
+
required: !0,
|
|
70
|
+
hint: "LMS's iss claim value.",
|
|
71
|
+
children: /* @__PURE__ */ g(b, {
|
|
72
|
+
id: "lti-issuer",
|
|
73
|
+
value: a,
|
|
74
|
+
onChange: (e) => o(e.target.value),
|
|
75
|
+
placeholder: "https://canvas.instructure.com"
|
|
76
|
+
})
|
|
77
|
+
}), /* @__PURE__ */ g(l, {
|
|
78
|
+
label: "Client ID",
|
|
79
|
+
htmlFor: "lti-clientid",
|
|
80
|
+
required: !0,
|
|
81
|
+
children: /* @__PURE__ */ g(b, {
|
|
82
|
+
id: "lti-clientid",
|
|
83
|
+
value: c,
|
|
84
|
+
onChange: (e) => f(e.target.value)
|
|
85
|
+
})
|
|
86
|
+
})]
|
|
87
|
+
}),
|
|
88
|
+
/* @__PURE__ */ g(l, {
|
|
89
|
+
label: "Deployment IDs",
|
|
90
|
+
htmlFor: "lti-deployments",
|
|
91
|
+
hint: "Comma-separated. Leave empty to accept any deployment from this platform.",
|
|
92
|
+
children: /* @__PURE__ */ g(b, {
|
|
93
|
+
id: "lti-deployments",
|
|
94
|
+
value: p,
|
|
95
|
+
onChange: (e) => m(e.target.value),
|
|
96
|
+
placeholder: "1, 2:abc123"
|
|
97
|
+
})
|
|
98
|
+
}),
|
|
99
|
+
/* @__PURE__ */ g(l, {
|
|
100
|
+
label: "OIDC auth login URL",
|
|
101
|
+
htmlFor: "lti-authlogin",
|
|
102
|
+
required: !0,
|
|
103
|
+
children: /* @__PURE__ */ g(b, {
|
|
104
|
+
id: "lti-authlogin",
|
|
105
|
+
value: y,
|
|
106
|
+
onChange: (e) => x(e.target.value),
|
|
107
|
+
placeholder: "https://canvas.instructure.com/api/lti/authorize_redirect"
|
|
108
|
+
})
|
|
109
|
+
}),
|
|
110
|
+
/* @__PURE__ */ g(l, {
|
|
111
|
+
label: "OAuth2 token URL",
|
|
112
|
+
htmlFor: "lti-authtoken",
|
|
113
|
+
required: !0,
|
|
114
|
+
hint: "Used for AGS grade passback.",
|
|
115
|
+
children: /* @__PURE__ */ g(b, {
|
|
116
|
+
id: "lti-authtoken",
|
|
117
|
+
value: S,
|
|
118
|
+
onChange: (e) => C(e.target.value),
|
|
119
|
+
placeholder: "https://canvas.instructure.com/login/oauth2/token"
|
|
120
|
+
})
|
|
121
|
+
}),
|
|
122
|
+
/* @__PURE__ */ g(l, {
|
|
123
|
+
label: "Platform JWKS URL",
|
|
124
|
+
htmlFor: "lti-keyset",
|
|
125
|
+
required: !0,
|
|
126
|
+
hint: "Used to verify launch id_tokens.",
|
|
127
|
+
children: /* @__PURE__ */ g(b, {
|
|
128
|
+
id: "lti-keyset",
|
|
129
|
+
value: w,
|
|
130
|
+
onChange: (e) => T(e.target.value),
|
|
131
|
+
placeholder: "https://canvas.instructure.com/api/lti/security/jwks"
|
|
132
|
+
})
|
|
133
|
+
}),
|
|
134
|
+
E && /* @__PURE__ */ g("p", {
|
|
135
|
+
className: "text-sm text-status-error-text",
|
|
136
|
+
children: E
|
|
137
|
+
})
|
|
138
|
+
]
|
|
139
|
+
})
|
|
140
|
+
});
|
|
141
|
+
}, S = ({ platform: e }) => {
|
|
142
|
+
let n = a(), [r, i] = d(!1), [o, s] = d(null);
|
|
143
|
+
return /* @__PURE__ */ _(y, {
|
|
144
|
+
treatment: "plain",
|
|
145
|
+
className: "flex flex-col gap-2 p-4",
|
|
146
|
+
children: [
|
|
147
|
+
/* @__PURE__ */ _("div", {
|
|
148
|
+
className: "flex flex-wrap items-start justify-between gap-2",
|
|
149
|
+
children: [/* @__PURE__ */ _("div", {
|
|
150
|
+
className: "min-w-0",
|
|
151
|
+
children: [/* @__PURE__ */ _("div", {
|
|
152
|
+
className: "flex items-center gap-2",
|
|
153
|
+
children: [/* @__PURE__ */ g("p", {
|
|
154
|
+
className: "truncate font-semibold text-text-primary",
|
|
155
|
+
children: e.name
|
|
156
|
+
}), /* @__PURE__ */ g("span", {
|
|
157
|
+
className: e.active ? "inline-flex items-center rounded-full bg-status-success-bg-subtle px-2.5 py-0.5 text-xs font-medium text-status-success-text" : "inline-flex items-center rounded-full border border-border-seam px-2.5 py-0.5 text-xs font-medium text-text-primary",
|
|
158
|
+
children: e.active ? "Active" : "Deactivated"
|
|
159
|
+
})]
|
|
160
|
+
}), /* @__PURE__ */ g("p", {
|
|
161
|
+
className: "truncate text-xs text-text-tertiary",
|
|
162
|
+
title: e.issuer,
|
|
163
|
+
children: e.issuer
|
|
164
|
+
})]
|
|
165
|
+
}), e.active && /* @__PURE__ */ g(c, {
|
|
166
|
+
resource: "buildmyiq-lti-platform",
|
|
167
|
+
action: "update",
|
|
168
|
+
deniedBehavior: "hide",
|
|
169
|
+
children: r ? /* @__PURE__ */ _("div", {
|
|
170
|
+
className: "flex items-center gap-2",
|
|
171
|
+
children: [/* @__PURE__ */ g(v, {
|
|
172
|
+
variant: "ghost",
|
|
173
|
+
size: "sm",
|
|
174
|
+
onClick: () => i(!1),
|
|
175
|
+
children: "Cancel"
|
|
176
|
+
}), /* @__PURE__ */ g(v, {
|
|
177
|
+
variant: "destructive",
|
|
178
|
+
size: "sm",
|
|
179
|
+
onClick: async () => {
|
|
180
|
+
s(null);
|
|
181
|
+
try {
|
|
182
|
+
await n.mutateAsync(e.id), i(!1);
|
|
183
|
+
} catch (e) {
|
|
184
|
+
s(e instanceof Error ? e.message : "Failed to deactivate.");
|
|
185
|
+
}
|
|
186
|
+
},
|
|
187
|
+
disabled: n.isPending,
|
|
188
|
+
children: n.isPending ? "Deactivating…" : "Confirm"
|
|
189
|
+
})]
|
|
190
|
+
}) : /* @__PURE__ */ _(v, {
|
|
191
|
+
variant: "outline",
|
|
192
|
+
size: "sm",
|
|
193
|
+
onClick: () => i(!0),
|
|
194
|
+
children: [/* @__PURE__ */ g(m, { className: "mr-1.5 h-3.5 w-3.5" }), " Deactivate"]
|
|
195
|
+
})
|
|
196
|
+
})]
|
|
197
|
+
}),
|
|
198
|
+
/* @__PURE__ */ _("dl", {
|
|
199
|
+
className: "grid grid-cols-2 gap-x-4 gap-y-1 text-xs text-text-secondary sm:grid-cols-4",
|
|
200
|
+
children: [
|
|
201
|
+
/* @__PURE__ */ _("div", { children: [/* @__PURE__ */ g("dt", {
|
|
202
|
+
className: "text-text-tertiary",
|
|
203
|
+
children: "Client ID"
|
|
204
|
+
}), /* @__PURE__ */ g("dd", {
|
|
205
|
+
className: "truncate font-mono",
|
|
206
|
+
title: e.clientId,
|
|
207
|
+
children: e.clientId
|
|
208
|
+
})] }),
|
|
209
|
+
/* @__PURE__ */ _("div", { children: [/* @__PURE__ */ g("dt", {
|
|
210
|
+
className: "text-text-tertiary",
|
|
211
|
+
children: "Deployments"
|
|
212
|
+
}), /* @__PURE__ */ g("dd", { children: e.deploymentIds.length > 0 ? e.deploymentIds.join(", ") : "Any" })] }),
|
|
213
|
+
/* @__PURE__ */ _("div", { children: [/* @__PURE__ */ g("dt", {
|
|
214
|
+
className: "text-text-tertiary",
|
|
215
|
+
children: "Registered"
|
|
216
|
+
}), /* @__PURE__ */ g("dd", { children: t(e.createdAt) })] })
|
|
217
|
+
]
|
|
218
|
+
}),
|
|
219
|
+
o && /* @__PURE__ */ g("p", {
|
|
220
|
+
className: "text-xs text-status-error-text",
|
|
221
|
+
children: o
|
|
222
|
+
})
|
|
223
|
+
]
|
|
224
|
+
});
|
|
225
|
+
}, C = () => {
|
|
226
|
+
let t = o(), [a, s] = d(!1), l = t.data ?? [];
|
|
227
|
+
return /* @__PURE__ */ _(e, {
|
|
228
|
+
title: "LMS integrations",
|
|
229
|
+
description: "Register LTI 1.3 Advantage platforms (Canvas, Moodle, Brightspace, …) that can launch into BuildMyIQ, embed assessments via Deep Linking, and receive grade passback.",
|
|
230
|
+
titleGradient: !0,
|
|
231
|
+
actions: /* @__PURE__ */ g(c, {
|
|
232
|
+
resource: "buildmyiq-lti-platform",
|
|
233
|
+
action: "create",
|
|
234
|
+
deniedBehavior: "hide",
|
|
235
|
+
children: /* @__PURE__ */ _(v, {
|
|
236
|
+
onClick: () => s(!0),
|
|
237
|
+
children: [/* @__PURE__ */ g(p, { className: "mr-1.5 h-4 w-4" }), " Register platform"]
|
|
238
|
+
})
|
|
239
|
+
}),
|
|
240
|
+
children: [t.isLoading ? /* @__PURE__ */ g(i, { label: "Loading LMS platforms…" }) : t.isError ? /* @__PURE__ */ g(r, {
|
|
241
|
+
message: "We couldn't load your LMS platforms.",
|
|
242
|
+
onRetry: () => t.refetch()
|
|
243
|
+
}) : l.length === 0 ? /* @__PURE__ */ g(n, {
|
|
244
|
+
illustration: "empty-data",
|
|
245
|
+
icon: /* @__PURE__ */ g(f, { className: "h-10 w-10" }),
|
|
246
|
+
title: "No LMS platforms registered",
|
|
247
|
+
description: "Register your institution's LMS to launch BuildMyIQ assessments from inside it, with automatic grade passback.",
|
|
248
|
+
action: /* @__PURE__ */ g(c, {
|
|
249
|
+
resource: "buildmyiq-lti-platform",
|
|
250
|
+
action: "create",
|
|
251
|
+
deniedBehavior: "hide",
|
|
252
|
+
children: /* @__PURE__ */ _(v, {
|
|
253
|
+
onClick: () => s(!0),
|
|
254
|
+
children: [/* @__PURE__ */ g(p, { className: "mr-1.5 h-4 w-4" }), " Register platform"]
|
|
255
|
+
})
|
|
256
|
+
})
|
|
257
|
+
}) : /* @__PURE__ */ g("div", {
|
|
258
|
+
className: "flex flex-col gap-3",
|
|
259
|
+
children: l.map((e) => /* @__PURE__ */ g(S, { platform: e }, e.id))
|
|
260
|
+
}), a && /* @__PURE__ */ g(x, {
|
|
261
|
+
onClose: () => s(!1),
|
|
262
|
+
onRegistered: () => s(!1)
|
|
263
|
+
})]
|
|
264
|
+
});
|
|
265
|
+
};
|
|
266
|
+
//#endregion
|
|
267
|
+
export { C as LtiPlatformsPage };
|
|
268
|
+
|
|
269
|
+
//# sourceMappingURL=LtiPlatformsPage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LtiPlatformsPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/integrations/LtiPlatformsPage.tsx"],"sourcesContent":["/**\n * LtiPlatformsPage — LTI 1.3 Advantage platform registry admin (BOFF-4419).\n * @module buildmyiq/pages/integrations\n *\n * Workspace-admin CRUD over `BuiLtiPlatform` — the LMS platforms allowed to\n * launch into this tool. Registering a platform here is the prerequisite for\n * an LMS instructor to embed a BuildMyIQ assessment via Deep Linking 2.0 and\n * receive AGS grade passback. The unauthenticated OIDC login-init/launch/JWKS\n * endpoints themselves are REST (`/lti/buildmyiq/*`) — this page only manages\n * the registry, it is not the launch flow.\n */\n\nimport type { FC } from 'react';\nimport { useState } from 'react';\nimport { Plug, Plus, Power } from 'lucide-react';\nimport { Button, GlassCard, Input } from '@burdenoff/fe-libs/ui';\nimport { PageLayout, EmptyState, LoadingView, ErrorView } from '../../components';\nimport { Modal, Field } from '../authoring/_forms';\nimport {\n useBuiLtiPlatforms,\n useBuiRegisterLtiPlatform,\n useBuiDeactivateLtiPlatform,\n} from '../../hooks';\nimport type { BuiLtiPlatform, BuiRegisterLtiPlatformInput } from '../../types';\nimport { formatDate } from '../../utils/formatters';\nimport { BuiPermissionButton } from '../../components/BuiPermissionButton';\n\nconst RegisterPlatformModal: FC<{ onClose: () => void; onRegistered: () => void }> = ({\n onClose,\n onRegistered,\n}) => {\n const register = useBuiRegisterLtiPlatform();\n const [name, setName] = useState('');\n const [issuer, setIssuer] = useState('');\n const [clientId, setClientId] = useState('');\n const [deploymentIds, setDeploymentIds] = useState('');\n const [authLoginUrl, setAuthLoginUrl] = useState('');\n const [authTokenUrl, setAuthTokenUrl] = useState('');\n const [keySetUrl, setKeySetUrl] = useState('');\n const [error, setError] = useState<string | null>(null);\n\n const canSubmit =\n name.trim() &&\n issuer.trim() &&\n clientId.trim() &&\n authLoginUrl.trim() &&\n authTokenUrl.trim() &&\n keySetUrl.trim() &&\n !register.isPending;\n\n const handleSubmit = async () => {\n if (!canSubmit) return;\n setError(null);\n const input: BuiRegisterLtiPlatformInput = {\n name: name.trim(),\n issuer: issuer.trim(),\n clientId: clientId.trim(),\n deploymentIds: deploymentIds\n .split(',')\n .map((s) => s.trim())\n .filter(Boolean),\n authLoginUrl: authLoginUrl.trim(),\n authTokenUrl: authTokenUrl.trim(),\n keySetUrl: keySetUrl.trim(),\n };\n try {\n await register.mutateAsync(input);\n onRegistered();\n } catch (e) {\n setError(e instanceof Error ? e.message : 'Failed to register the platform.');\n }\n };\n\n return (\n <Modal\n open\n onClose={onClose}\n title=\"Register LMS platform\"\n description=\"Add the LTI 1.3 Advantage platform details from your LMS admin console (e.g. Canvas, Moodle, Brightspace).\"\n className=\"sm:max-w-xl\"\n footer={\n <>\n <Button variant=\"ghost\" onClick={onClose} disabled={register.isPending}>\n Cancel\n </Button>\n <Button onClick={handleSubmit} disabled={!canSubmit}>\n {register.isPending ? 'Registering…' : 'Register platform'}\n </Button>\n </>\n }\n >\n <div className=\"grid gap-4\">\n <Field label=\"Platform name\" htmlFor=\"lti-name\" required>\n <Input\n id=\"lti-name\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n placeholder=\"e.g. Acme University Canvas\"\n />\n </Field>\n <div className=\"grid gap-4 sm:grid-cols-2\">\n <Field label=\"Issuer\" htmlFor=\"lti-issuer\" required hint=\"LMS's iss claim value.\">\n <Input\n id=\"lti-issuer\"\n value={issuer}\n onChange={(e) => setIssuer(e.target.value)}\n placeholder=\"https://canvas.instructure.com\"\n />\n </Field>\n <Field label=\"Client ID\" htmlFor=\"lti-clientid\" required>\n <Input\n id=\"lti-clientid\"\n value={clientId}\n onChange={(e) => setClientId(e.target.value)}\n />\n </Field>\n </div>\n <Field\n label=\"Deployment IDs\"\n htmlFor=\"lti-deployments\"\n hint=\"Comma-separated. Leave empty to accept any deployment from this platform.\"\n >\n <Input\n id=\"lti-deployments\"\n value={deploymentIds}\n onChange={(e) => setDeploymentIds(e.target.value)}\n placeholder=\"1, 2:abc123\"\n />\n </Field>\n <Field label=\"OIDC auth login URL\" htmlFor=\"lti-authlogin\" required>\n <Input\n id=\"lti-authlogin\"\n value={authLoginUrl}\n onChange={(e) => setAuthLoginUrl(e.target.value)}\n placeholder=\"https://canvas.instructure.com/api/lti/authorize_redirect\"\n />\n </Field>\n <Field\n label=\"OAuth2 token URL\"\n htmlFor=\"lti-authtoken\"\n required\n hint=\"Used for AGS grade passback.\"\n >\n <Input\n id=\"lti-authtoken\"\n value={authTokenUrl}\n onChange={(e) => setAuthTokenUrl(e.target.value)}\n placeholder=\"https://canvas.instructure.com/login/oauth2/token\"\n />\n </Field>\n <Field\n label=\"Platform JWKS URL\"\n htmlFor=\"lti-keyset\"\n required\n hint=\"Used to verify launch id_tokens.\"\n >\n <Input\n id=\"lti-keyset\"\n value={keySetUrl}\n onChange={(e) => setKeySetUrl(e.target.value)}\n placeholder=\"https://canvas.instructure.com/api/lti/security/jwks\"\n />\n </Field>\n {error && <p className=\"text-sm text-status-error-text\">{error}</p>}\n </div>\n </Modal>\n );\n};\n\nconst PlatformRow: FC<{ platform: BuiLtiPlatform }> = ({ platform }) => {\n const deactivate = useBuiDeactivateLtiPlatform();\n const [confirming, setConfirming] = useState(false);\n const [error, setError] = useState<string | null>(null);\n\n const handleDeactivate = async () => {\n setError(null);\n try {\n await deactivate.mutateAsync(platform.id);\n setConfirming(false);\n } catch (e) {\n setError(e instanceof Error ? e.message : 'Failed to deactivate.');\n }\n };\n\n return (\n <GlassCard treatment=\"plain\" className=\"flex flex-col gap-2 p-4\">\n <div className=\"flex flex-wrap items-start justify-between gap-2\">\n <div className=\"min-w-0\">\n <div className=\"flex items-center gap-2\">\n <p className=\"truncate font-semibold text-text-primary\">{platform.name}</p>\n <span\n className={\n platform.active\n ? 'inline-flex items-center rounded-full bg-status-success-bg-subtle px-2.5 py-0.5 text-xs font-medium text-status-success-text'\n : 'inline-flex items-center rounded-full border border-border-seam px-2.5 py-0.5 text-xs font-medium text-text-primary'\n }\n >\n {platform.active ? 'Active' : 'Deactivated'}\n </span>\n </div>\n <p className=\"truncate text-xs text-text-tertiary\" title={platform.issuer}>\n {platform.issuer}\n </p>\n </div>\n {platform.active && (\n <BuiPermissionButton\n resource=\"buildmyiq-lti-platform\"\n action=\"update\"\n deniedBehavior=\"hide\"\n >\n {confirming ? (\n <div className=\"flex items-center gap-2\">\n <Button variant=\"ghost\" size=\"sm\" onClick={() => setConfirming(false)}>\n Cancel\n </Button>\n <Button\n variant=\"destructive\"\n size=\"sm\"\n onClick={handleDeactivate}\n disabled={deactivate.isPending}\n >\n {deactivate.isPending ? 'Deactivating…' : 'Confirm'}\n </Button>\n </div>\n ) : (\n <Button variant=\"outline\" size=\"sm\" onClick={() => setConfirming(true)}>\n <Power className=\"mr-1.5 h-3.5 w-3.5\" /> Deactivate\n </Button>\n )}\n </BuiPermissionButton>\n )}\n </div>\n <dl className=\"grid grid-cols-2 gap-x-4 gap-y-1 text-xs text-text-secondary sm:grid-cols-4\">\n <div>\n <dt className=\"text-text-tertiary\">Client ID</dt>\n <dd className=\"truncate font-mono\" title={platform.clientId}>\n {platform.clientId}\n </dd>\n </div>\n <div>\n <dt className=\"text-text-tertiary\">Deployments</dt>\n <dd>{platform.deploymentIds.length > 0 ? platform.deploymentIds.join(', ') : 'Any'}</dd>\n </div>\n <div>\n <dt className=\"text-text-tertiary\">Registered</dt>\n <dd>{formatDate(platform.createdAt)}</dd>\n </div>\n </dl>\n {error && <p className=\"text-xs text-status-error-text\">{error}</p>}\n </GlassCard>\n );\n};\n\nexport const LtiPlatformsPage: FC = () => {\n const platformsQuery = useBuiLtiPlatforms();\n const [showRegister, setShowRegister] = useState(false);\n const platforms = platformsQuery.data ?? [];\n\n return (\n <PageLayout\n title=\"LMS integrations\"\n description=\"Register LTI 1.3 Advantage platforms (Canvas, Moodle, Brightspace, …) that can launch into BuildMyIQ, embed assessments via Deep Linking, and receive grade passback.\"\n titleGradient\n actions={\n <BuiPermissionButton\n resource=\"buildmyiq-lti-platform\"\n action=\"create\"\n deniedBehavior=\"hide\"\n >\n <Button onClick={() => setShowRegister(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Register platform\n </Button>\n </BuiPermissionButton>\n }\n >\n {platformsQuery.isLoading ? (\n <LoadingView label=\"Loading LMS platforms…\" />\n ) : platformsQuery.isError ? (\n <ErrorView\n message=\"We couldn't load your LMS platforms.\"\n onRetry={() => platformsQuery.refetch()}\n />\n ) : platforms.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n icon={<Plug className=\"h-10 w-10\" />}\n title=\"No LMS platforms registered\"\n description=\"Register your institution's LMS to launch BuildMyIQ assessments from inside it, with automatic grade passback.\"\n action={\n <BuiPermissionButton\n resource=\"buildmyiq-lti-platform\"\n action=\"create\"\n deniedBehavior=\"hide\"\n >\n <Button onClick={() => setShowRegister(true)}>\n <Plus className=\"mr-1.5 h-4 w-4\" /> Register platform\n </Button>\n </BuiPermissionButton>\n }\n />\n ) : (\n <div className=\"flex flex-col gap-3\">\n {platforms.map((p) => (\n <PlatformRow key={p.id} platform={p} />\n ))}\n </div>\n )}\n\n {showRegister && (\n <RegisterPlatformModal\n onClose={() => setShowRegister(false)}\n onRegistered={() => setShowRegister(false)}\n />\n )}\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|