@burdenoff/microfe-buildmyiq 2026.803.1 → 2026.804.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.
Files changed (43) hide show
  1. package/dist/buildmyiq/hooks/useBuildmyiqApi.d.ts +21 -3
  2. package/dist/buildmyiq/hooks/useBuildmyiqApi.d.ts.map +1 -1
  3. package/dist/buildmyiq/hooks/useBuildmyiqApi.js +116 -62
  4. package/dist/buildmyiq/hooks/useBuildmyiqApi.js.map +1 -1
  5. package/dist/buildmyiq/pages/DashboardPage.d.ts.map +1 -1
  6. package/dist/buildmyiq/pages/DashboardPage.js +3 -4
  7. package/dist/buildmyiq/pages/DashboardPage.js.map +1 -1
  8. package/dist/buildmyiq/pages/assessments/TakeAssessmentPage.d.ts.map +1 -1
  9. package/dist/buildmyiq/pages/assessments/TakeAssessmentPage.js +1 -2
  10. package/dist/buildmyiq/pages/assessments/TakeAssessmentPage.js.map +1 -1
  11. package/dist/buildmyiq/pages/catalog/CourseDetailPage.d.ts.map +1 -1
  12. package/dist/buildmyiq/pages/catalog/CourseDetailPage.js +14 -15
  13. package/dist/buildmyiq/pages/catalog/CourseDetailPage.js.map +1 -1
  14. package/dist/buildmyiq/pages/certificates/CertificatesPage.d.ts.map +1 -1
  15. package/dist/buildmyiq/pages/certificates/CertificatesPage.js +175 -175
  16. package/dist/buildmyiq/pages/certificates/CertificatesPage.js.map +1 -1
  17. package/dist/buildmyiq/pages/learning/MyLearningPage.js +2 -4
  18. package/dist/buildmyiq/pages/learning/MyLearningPage.js.map +1 -1
  19. package/dist/buildmyiq/pages/paths/PathsPage.d.ts.map +1 -1
  20. package/dist/buildmyiq/pages/paths/PathsPage.js +1 -2
  21. package/dist/buildmyiq/pages/paths/PathsPage.js.map +1 -1
  22. package/dist/buildmyiq/pages/submissions/AnswersView.d.ts +6 -0
  23. package/dist/buildmyiq/pages/submissions/AnswersView.d.ts.map +1 -0
  24. package/dist/buildmyiq/pages/submissions/AnswersView.js +41 -0
  25. package/dist/buildmyiq/pages/submissions/AnswersView.js.map +1 -0
  26. package/dist/buildmyiq/pages/submissions/BulkGradingPage.d.ts.map +1 -1
  27. package/dist/buildmyiq/pages/submissions/BulkGradingPage.js +332 -173
  28. package/dist/buildmyiq/pages/submissions/BulkGradingPage.js.map +1 -1
  29. package/dist/buildmyiq/pages/submissions/SubmissionsPage.d.ts.map +1 -1
  30. package/dist/buildmyiq/pages/submissions/SubmissionsPage.js +221 -227
  31. package/dist/buildmyiq/pages/submissions/SubmissionsPage.js.map +1 -1
  32. package/dist/buildmyiq/pages/submissions/aiGrading.d.ts +26 -0
  33. package/dist/buildmyiq/pages/submissions/aiGrading.d.ts.map +1 -0
  34. package/dist/buildmyiq/pages/submissions/aiGrading.js +23 -0
  35. package/dist/buildmyiq/pages/submissions/aiGrading.js.map +1 -0
  36. package/dist/buildmyiq/types/index.d.ts +10 -0
  37. package/dist/buildmyiq/types/index.d.ts.map +1 -1
  38. package/dist/buildmyiq/types/index.js.map +1 -1
  39. package/dist/utils/pageContext.d.ts +8 -0
  40. package/dist/utils/pageContext.d.ts.map +1 -1
  41. package/dist/utils/pageContext.js +8 -0
  42. package/dist/utils/pageContext.js.map +1 -1
  43. package/package.json +2 -1
@@ -1 +1 @@
1
- {"version":3,"file":"useBuildmyiqApi.js","names":[],"sources":["../../../src/buildmyiq/hooks/useBuildmyiqApi.ts"],"sourcesContent":["/**\n * Buildmyiq API hooks\n * @module buildmyiq/hooks\n *\n * React-Query hooks wired DIRECTLY to the real `wspace-buildmyiq-svc`\n * federated GraphQL contract (the `bui*` operations). Every query/mutation\n * below uses an inline gql string whose fields match the svc\n * `.hive-schema.graphql` exactly. No mock data, no codegen.\n *\n * Transport: `graphqlFetch({ gateway: 'workspace', workspaceToken: true })` —\n * the host shell supplies the gateway base URL + a workspace-scoped token.\n */\n\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\n\nimport { useBuildmyiq } from '../context';\nimport type {\n BuiConnection,\n BuiCourse,\n BuiCourseLevel,\n BuiCourseStatus,\n BuiModule,\n BuiLesson,\n BuiAssessment,\n BuiAssessmentStatus,\n BuiQuestion,\n BuiSubmission,\n BuiSubmissionStatus,\n BuiEnrollment,\n BuiEnrollmentStatus,\n BuiCohort,\n BuiCohortStatus,\n BuiSkill,\n BuiSkillCategory,\n BuiSkillStandard,\n BuiCertification,\n BuiSkillMappingSuggestion,\n BuiCertificate,\n BuiLessonProgress,\n BuiCreateCourseInput,\n BuiUpdateCourseInput,\n BuiCreateModuleInput,\n BuiCreateLessonInput,\n BuiCreateAssessmentInput,\n BuiCreateQuestionInput,\n BuiCreateSkillInput,\n BuiCreateCohortInput,\n BuiEnrollInput,\n BuiRecordLessonProgressInput,\n BuiSubmitAssessmentInput,\n BuiGradeSubmissionInput,\n BuiIssueCertificateInput,\n BuiTutorAskInput,\n BuiTutorAskPayload,\n BuiTutorSession,\n BuiGradeSuggestion,\n BuiCourseOutlineSuggestion,\n BuiApplyCourseOutlineInput,\n BuiAdminCopilotAnswer,\n BuiAdminCopilotTurnInput,\n BuiAiSuggestionOutcome,\n BuiLtiPlatform,\n BuiRegisterLtiPlatformInput,\n PaginationInput,\n} from '../types';\n\n// ============================================================================\n// Transport\n// ============================================================================\n\n/**\n * fe-libs `graphqlFetch` coalesces `query` responses in an in-memory cache keyed\n * by operationName+variables for ~15s (mutations are never cached). A read\n * issued right after a write (e.g. the React-Query refetch triggered by our\n * `invalidateQueries`) would otherwise return the PRE-mutation response until the\n * TTL expires — making just-created/updated rows invisible until a full reload.\n * We clear those caches after every buildmyiq mutation so every follow-up read\n * is fresh.\n */\ntype GraphqlReadCacheGlobals = {\n __graphqlFetchResponseCache?: { clear: () => void };\n __graphqlFetchPendingResponses?: { clear: () => void };\n __boffGraphQLResponseCache?: { clear: () => void };\n};\n\nconst clearGraphqlReadCaches = (): void => {\n const g = globalThis as GraphqlReadCacheGlobals;\n g.__graphqlFetchResponseCache?.clear();\n g.__graphqlFetchPendingResponses?.clear();\n g.__boffGraphQLResponseCache?.clear();\n};\n\nconst buiQuery = async <TData>(\n query: string,\n variables: Record<string, unknown>,\n baseUrl?: string\n): Promise<TData> => {\n const response = await graphqlFetch<TData>({\n gateway: 'workspace',\n baseUrl,\n query,\n variables,\n workspaceToken: true,\n });\n\n if (response.errors?.length) {\n throw new Error(response.errors[0]?.message ?? 'GraphQL request failed');\n }\n if (!response.data) {\n throw new Error('GraphQL response did not include data');\n }\n if (/^\\s*mutation\\b/.test(query)) {\n clearGraphqlReadCaches();\n }\n return response.data;\n};\n\n// React-Query key roots (one namespace per domain entity).\nconst KEY = {\n courses: ['buildmyiq', 'courses'] as const,\n course: (id: string) => ['buildmyiq', 'course', id] as const,\n modules: (courseId: string) => ['buildmyiq', 'modules', courseId] as const,\n lessons: (courseId: string) => ['buildmyiq', 'lessons', courseId] as const,\n assessments: (courseId: string) => ['buildmyiq', 'assessments', courseId] as const,\n assessment: (id: string) => ['buildmyiq', 'assessment', id] as const,\n enrollments: ['buildmyiq', 'enrollments'] as const,\n enrollment: (id: string) => ['buildmyiq', 'enrollment', id] as const,\n certificates: ['buildmyiq', 'certificates'] as const,\n cohorts: ['buildmyiq', 'cohorts'] as const,\n skills: ['buildmyiq', 'skills'] as const,\n submissions: ['buildmyiq', 'submissions'] as const,\n submission: (id: string) => ['buildmyiq', 'submission', id] as const,\n tutorSessions: (lessonId: string) => ['buildmyiq', 'tutorSessions', lessonId] as const,\n ltiPlatforms: ['buildmyiq', 'ltiPlatforms'] as const,\n};\n\nconst DEFAULT_PAGE_SIZE = 24;\n\n/**\n * Build a `PaginationInput` for a list query. The FIRST page (undefined cursor)\n * OMITS the cursor key entirely — the svc rejects an explicit `cursor: null`.\n */\nconst pageVars = (cursor?: string, limit = DEFAULT_PAGE_SIZE): PaginationInput =>\n cursor ? { limit, cursor } : { limit };\n\n// ============================================================================\n// Reusable gql field selections (codegen-free strictness)\n// ============================================================================\n\nconst FILE_FIELDS = `id name downloadUrl mimeType`;\n\nconst SKILL_FIELDS = `\n id workspaceId name category description isActive\n onetCode escoUri socCodes certificationIds taxonomyMetadata\n createdAt updatedAt\n`;\n\nconst SKILL_STANDARD_FIELDS = `\n id workspaceId source code title description category uri socCodes metadata isActive createdAt updatedAt\n`;\n\nconst CERTIFICATION_FIELDS = `\n id workspaceId body code name description url isActive createdAt updatedAt\n`;\n\nconst COURSE_CORE_FIELDS = `\n id workspaceId title slug summary description status level language\n estimatedHours isPublic tags skillId instructorId publishedAt createdAt updatedAt\n cover { ${FILE_FIELDS} }\n skill { id name category }\n`;\n\nconst MODULE_FIELDS = `\n id workspaceId courseId title description position createdAt updatedAt\n`;\n\nconst LESSON_FIELDS = `\n id workspaceId courseId moduleId title type content durationMin position isPreview\n createdAt updatedAt\n media { ${FILE_FIELDS} }\n`;\n\n// NOTE: `modules`/`lessons` are `Bui*Connection!` on the svc schema (paginated),\n// NOT plain lists — every nested selection below must go through `items { }` or\n// the gateway rejects the whole query with GRAPHQL_VALIDATION_FAILED (BOFF-4703\n// Connection-shape drift). See `flattenCourse` for the matching unwrap.\nconst COURSE_FULL_FIELDS = `\n ${COURSE_CORE_FIELDS}\n modules { items { ${MODULE_FIELDS} lessons { items { ${LESSON_FIELDS} } } } }\n lessons { items { ${LESSON_FIELDS} } }\n`;\n\nconst ASSESSMENT_FIELDS = `\n id workspaceId courseId title description type status passingScore maxAttempts\n timeLimitMin isGraded createdAt updatedAt\n`;\n\n// NOTE: BuiQuestion has NO `answerKey` on the type (create-input only).\nconst QUESTION_FIELDS = `\n id workspaceId assessmentId prompt type options points position explanation\n createdAt updatedAt\n`;\n\n// `questions` is `BuiQuestionConnection!` — see COURSE_FULL_FIELDS note above.\nconst ASSESSMENT_FULL_FIELDS = `\n ${ASSESSMENT_FIELDS}\n questions { items { ${QUESTION_FIELDS} } }\n`;\n\nconst SUBMISSION_FIELDS = `\n id workspaceId assessmentId enrollmentId learnerId attempt answers status score\n passed feedback gradedById gradedAt submittedAt createdAt updatedAt\n`;\n\nconst COHORT_FIELDS = `\n id workspaceId courseId name capacity facilitatorId startDate endDate status\n createdAt updatedAt\n`;\n\nconst LESSON_PROGRESS_FIELDS = `\n id workspaceId enrollmentId lessonId status secondsSpent completedAt createdAt updatedAt\n`;\n\nconst ENROLLMENT_FIELDS = `\n id workspaceId courseId learnerId cohortId status progressPct enrolledAt startedAt\n completedAt lastActiveAt expiresAt createdAt updatedAt\n`;\n\n// `lessonProgress` is `BuiLessonProgressConnection!` — see COURSE_FULL_FIELDS note above.\nconst ENROLLMENT_FULL_FIELDS = `\n ${ENROLLMENT_FIELDS}\n cohort { ${COHORT_FIELDS} }\n lessonProgress { items { ${LESSON_PROGRESS_FIELDS} } }\n`;\n\nconst CERTIFICATE_FIELDS = `\n id workspaceId courseId learnerId enrollmentId serial status issuedAt expiresAt\n revokedAt finalScore createdAt updatedAt\n file { ${FILE_FIELDS} }\n`;\n\nconst TUTOR_MESSAGE_FIELDS = `\n id workspaceId sessionId role content groundingLessonIds adaptiveLevel createdAt\n`;\n\nconst TUTOR_SESSION_FIELDS = `\n id workspaceId courseId lessonId learnerId createdAt updatedAt\n messages { ${TUTOR_MESSAGE_FIELDS} }\n`;\n\n// ============================================================================\n// Connection-shape unwrap (BOFF-4703)\n// ============================================================================\n// Every `Bui*Connection` field on the wire is `{ items: T[]; pageInfo }`, never\n// a bare list — including NESTED fields like `BuiCourse.modules` or\n// `BuiAssessment.questions`. The hooks below keep returning plain flat arrays\n// to consumers (that's the shape every page in this MFE is written against),\n// so raw wire responses are unwrapped once here rather than pushing the\n// Connection shape out to every page.\n\n/** Wire shape of a `BuiModule` fetched via `COURSE_FULL_FIELDS` (nested `lessons` is a Connection). */\ntype BuiModuleWire = Omit<BuiModule, 'lessons'> & { lessons: { items: BuiLesson[] } };\n\n/** Wire shape of a `BuiCourse` fetched via `COURSE_FULL_FIELDS`. */\ntype BuiCourseWire = Omit<BuiCourse, 'modules' | 'lessons'> & {\n modules: { items: BuiModuleWire[] };\n lessons: { items: BuiLesson[] };\n};\n\nconst flattenCourse = (c: BuiCourseWire): BuiCourse => ({\n ...c,\n modules: c.modules.items.map((m) => ({ ...m, lessons: m.lessons.items })),\n lessons: c.lessons.items,\n});\n\n/** Wire shape of a `BuiAssessment` fetched via `ASSESSMENT_FULL_FIELDS`. */\ntype BuiAssessmentWire = Omit<BuiAssessment, 'questions'> & { questions: { items: BuiQuestion[] } };\n\nconst flattenAssessment = (a: BuiAssessmentWire): BuiAssessment => ({\n ...a,\n questions: a.questions.items,\n});\n\n/** Wire shape of a `BuiEnrollment` fetched via `ENROLLMENT_FULL_FIELDS`. */\ntype BuiEnrollmentWire = Omit<BuiEnrollment, 'lessonProgress'> & {\n lessonProgress: { items: BuiLessonProgress[] };\n};\n\nconst flattenEnrollment = (e: BuiEnrollmentWire): BuiEnrollment => ({\n ...e,\n lessonProgress: e.lessonProgress.items,\n});\n\n// ============================================================================\n// Shared hook helper: resolve gateway URL from context\n// ============================================================================\n\nconst useGateway = (): string | undefined => {\n const { apiGatewayUrl } = useBuildmyiq();\n return apiGatewayUrl;\n};\n\n// ============================================================================\n// Courses\n// ============================================================================\n\nexport interface UseBuiCoursesArgs {\n level?: BuiCourseLevel;\n status?: BuiCourseStatus;\n cursor?: string;\n limit?: number;\n enabled?: boolean;\n}\n\nexport const useBuiCourses = (args: UseBuiCoursesArgs = {}) => {\n const baseUrl = useGateway();\n const { level, status, cursor, limit, enabled = true } = args;\n return useQuery({\n queryKey: [...KEY.courses, { level, status, cursor, limit }],\n enabled,\n queryFn: async () => {\n // Only `id` is needed here (list view shows module/lesson COUNTS, not\n // full records — see CourseCard.tsx) so we intentionally under-select\n // vs. the full `BuiModule`/`BuiLesson` shape; still must go through\n // `items { }` since both fields are Connections (BOFF-4703).\n const data = await buiQuery<{\n buiListCourses: {\n items: Array<\n Omit<BuiCourse, 'modules' | 'lessons'> & {\n modules: { items: Array<{ id: string }> };\n lessons: { items: Array<{ id: string }> };\n }\n >;\n pageInfo: BuiConnection<BuiCourse>['pageInfo'];\n };\n }>(\n `query BuiListCourses($level: BuiCourseLevel, $status: BuiCourseStatus, $pagination: PaginationInput) {\n buiListCourses(level: $level, status: $status, pagination: $pagination) {\n items { ${COURSE_CORE_FIELDS} modules { items { id } } lessons { items { id } } }\n pageInfo { cursor hasMore }\n }\n }`,\n { level, status, pagination: pageVars(cursor, limit) },\n baseUrl\n );\n return {\n items: data.buiListCourses.items.map((c) => ({\n ...c,\n // Partial `{ id }`-only records are cast to the full type — safe here\n // because every consumer of the list view only reads `.length`.\n modules: c.modules.items as unknown as BuiModule[],\n lessons: c.lessons.items as unknown as BuiLesson[],\n })),\n pageInfo: data.buiListCourses.pageInfo,\n };\n },\n });\n};\n\nexport const useBuiCourse = (id: string | undefined) => {\n const baseUrl = useGateway();\n return useQuery({\n queryKey: id ? KEY.course(id) : ['buildmyiq', 'course', 'nil'],\n enabled: Boolean(id),\n queryFn: async () => {\n const data = await buiQuery<{ buiCourse: BuiCourseWire | null }>(\n `query BuiCourse($id: ID!) {\n buiCourse(id: $id) { ${COURSE_FULL_FIELDS} }\n }`,\n { id },\n baseUrl\n );\n return data.buiCourse ? flattenCourse(data.buiCourse) : null;\n },\n });\n};\n\nexport const useBuiCreateCourse = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: BuiCreateCourseInput) => {\n const data = await buiQuery<{ buiCreateCourse: BuiCourse }>(\n `mutation BuiCreateCourse($input: BuiCreateCourseInput!) {\n buiCreateCourse(input: $input) { ${COURSE_CORE_FIELDS} }\n }`,\n { input },\n baseUrl\n );\n return data.buiCreateCourse;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.courses }),\n });\n};\n\nexport const useBuiUpdateCourse = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (args: { id: string; input: BuiUpdateCourseInput }) => {\n const data = await buiQuery<{ buiUpdateCourse: BuiCourse }>(\n `mutation BuiUpdateCourse($id: ID!, $input: BuiUpdateCourseInput!) {\n buiUpdateCourse(id: $id, input: $input) { ${COURSE_CORE_FIELDS} }\n }`,\n args,\n baseUrl\n );\n return data.buiUpdateCourse;\n },\n onSuccess: (course) => {\n qc.invalidateQueries({ queryKey: KEY.courses });\n qc.invalidateQueries({ queryKey: KEY.course(course.id) });\n },\n });\n};\n\nexport const useBuiPublishCourse = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (id: string) => {\n const data = await buiQuery<{ buiPublishCourse: BuiCourse }>(\n `mutation BuiPublishCourse($id: ID!) {\n buiPublishCourse(id: $id) { ${COURSE_CORE_FIELDS} }\n }`,\n { id },\n baseUrl\n );\n return data.buiPublishCourse;\n },\n onSuccess: (course) => {\n qc.invalidateQueries({ queryKey: KEY.courses });\n qc.invalidateQueries({ queryKey: KEY.course(course.id) });\n },\n });\n};\n\nexport const useBuiArchiveCourse = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (id: string) => {\n const data = await buiQuery<{ buiArchiveCourse: boolean }>(\n `mutation BuiArchiveCourse($id: ID!) { buiArchiveCourse(id: $id) }`,\n { id },\n baseUrl\n );\n return data.buiArchiveCourse;\n },\n onSuccess: (_r, id) => {\n qc.invalidateQueries({ queryKey: KEY.courses });\n qc.invalidateQueries({ queryKey: KEY.course(id) });\n },\n });\n};\n\n// ============================================================================\n// Modules & lessons\n// ============================================================================\n\nexport const useBuiModules = (courseId: string | undefined) => {\n const baseUrl = useGateway();\n return useQuery({\n queryKey: courseId ? KEY.modules(courseId) : ['buildmyiq', 'modules', 'nil'],\n enabled: Boolean(courseId),\n queryFn: async () => {\n const data = await buiQuery<{ buiListModules: { items: BuiModuleWire[] } }>(\n `query BuiListModules($courseId: ID!) {\n buiListModules(courseId: $courseId) {\n items { ${MODULE_FIELDS} lessons { items { ${LESSON_FIELDS} } } }\n }\n }`,\n { courseId },\n baseUrl\n );\n return data.buiListModules.items.map((m) => ({ ...m, lessons: m.lessons.items }));\n },\n });\n};\n\nexport const useBuiLessons = (courseId: string | undefined, moduleId?: string) => {\n const baseUrl = useGateway();\n return useQuery({\n queryKey: courseId ? [...KEY.lessons(courseId), { moduleId }] : ['buildmyiq', 'lessons', 'nil'],\n enabled: Boolean(courseId),\n queryFn: async () => {\n const data = await buiQuery<{ buiListLessons: { items: BuiLesson[] } }>(\n `query BuiListLessons($courseId: ID!, $moduleId: ID) {\n buiListLessons(courseId: $courseId, moduleId: $moduleId) { items { ${LESSON_FIELDS} } }\n }`,\n { courseId, moduleId },\n baseUrl\n );\n return data.buiListLessons.items;\n },\n });\n};\n\nexport const useBuiCreateModule = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: BuiCreateModuleInput) => {\n const data = await buiQuery<{ buiCreateModule: BuiModule }>(\n `mutation BuiCreateModule($input: BuiCreateModuleInput!) {\n buiCreateModule(input: $input) { ${MODULE_FIELDS} }\n }`,\n { input },\n baseUrl\n );\n return data.buiCreateModule;\n },\n onSuccess: (m) => {\n qc.invalidateQueries({ queryKey: KEY.modules(m.courseId) });\n qc.invalidateQueries({ queryKey: KEY.course(m.courseId) });\n },\n });\n};\n\nexport const useBuiCreateLesson = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: BuiCreateLessonInput) => {\n const data = await buiQuery<{ buiCreateLesson: BuiLesson }>(\n `mutation BuiCreateLesson($input: BuiCreateLessonInput!) {\n buiCreateLesson(input: $input) { ${LESSON_FIELDS} }\n }`,\n { input },\n baseUrl\n );\n return data.buiCreateLesson;\n },\n onSuccess: (l) => {\n qc.invalidateQueries({ queryKey: KEY.lessons(l.courseId) });\n qc.invalidateQueries({ queryKey: KEY.modules(l.courseId) });\n qc.invalidateQueries({ queryKey: KEY.course(l.courseId) });\n },\n });\n};\n\n// ============================================================================\n// Assessments & questions\n// ============================================================================\n\nexport const useBuiAssessments = (courseId: string | undefined, status?: BuiAssessmentStatus) => {\n const baseUrl = useGateway();\n return useQuery({\n queryKey: courseId\n ? [...KEY.assessments(courseId), { status }]\n : ['buildmyiq', 'assessments', 'nil'],\n enabled: Boolean(courseId),\n queryFn: async () => {\n // Only `id` is needed here (list view shows a question COUNT, not full\n // records — see AssessmentsPage.tsx); still must go through `items { }`\n // since `questions` is a Connection (BOFF-4703).\n const data = await buiQuery<{\n buiListAssessments: {\n items: Array<\n Omit<BuiAssessment, 'questions'> & { questions: { items: Array<{ id: string }> } }\n >;\n };\n }>(\n `query BuiListAssessments($courseId: ID!, $status: BuiAssessmentStatus) {\n buiListAssessments(courseId: $courseId, status: $status) {\n items { ${ASSESSMENT_FIELDS} questions { items { id } } }\n }\n }`,\n { courseId, status },\n baseUrl\n );\n return data.buiListAssessments.items.map((a) => ({\n ...a,\n // Partial `{ id }`-only records are cast to the full type — safe here\n // because every consumer of the list view only reads `.length`.\n questions: a.questions.items as unknown as BuiQuestion[],\n }));\n },\n });\n};\n\nexport const useBuiAssessment = (id: string | undefined) => {\n const baseUrl = useGateway();\n return useQuery({\n queryKey: id ? KEY.assessment(id) : ['buildmyiq', 'assessment', 'nil'],\n enabled: Boolean(id),\n queryFn: async () => {\n const data = await buiQuery<{ buiAssessment: BuiAssessmentWire | null }>(\n `query BuiAssessment($id: ID!) {\n buiAssessment(id: $id) { ${ASSESSMENT_FULL_FIELDS} }\n }`,\n { id },\n baseUrl\n );\n return data.buiAssessment ? flattenAssessment(data.buiAssessment) : null;\n },\n });\n};\n\nexport const useBuiCreateAssessment = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: BuiCreateAssessmentInput) => {\n const data = await buiQuery<{ buiCreateAssessment: BuiAssessment }>(\n `mutation BuiCreateAssessment($input: BuiCreateAssessmentInput!) {\n buiCreateAssessment(input: $input) { ${ASSESSMENT_FIELDS} }\n }`,\n { input },\n baseUrl\n );\n return data.buiCreateAssessment;\n },\n onSuccess: (a) => qc.invalidateQueries({ queryKey: KEY.assessments(a.courseId) }),\n });\n};\n\nexport const useBuiPublishAssessment = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (id: string) => {\n const data = await buiQuery<{ buiPublishAssessment: BuiAssessment }>(\n `mutation BuiPublishAssessment($id: ID!) {\n buiPublishAssessment(id: $id) { ${ASSESSMENT_FIELDS} }\n }`,\n { id },\n baseUrl\n );\n return data.buiPublishAssessment;\n },\n onSuccess: (a) => {\n qc.invalidateQueries({ queryKey: KEY.assessment(a.id) });\n qc.invalidateQueries({ queryKey: KEY.assessments(a.courseId) });\n },\n });\n};\n\n/**\n * Archive an assessment (soft-delete) — releases its reusable\n * `buildmyiq.assessments.count` quota slot server-side (BOFF-4722) so the\n * workspace can create a replacement without hitting the plan limit.\n * `buiArchiveAssessment` returns only a `Boolean!`, not the record, so the\n * caller supplies `courseId` (already in hand from the loaded assessment)\n * to invalidate the right list query.\n */\nexport const useBuiArchiveAssessment = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (args: { id: string; courseId: string }) => {\n const data = await buiQuery<{ buiArchiveAssessment: boolean }>(\n `mutation BuiArchiveAssessment($id: ID!) { buiArchiveAssessment(id: $id) }`,\n { id: args.id },\n baseUrl\n );\n return data.buiArchiveAssessment;\n },\n onSuccess: (_r, args) => {\n qc.invalidateQueries({ queryKey: KEY.assessment(args.id) });\n qc.invalidateQueries({ queryKey: KEY.assessments(args.courseId) });\n },\n });\n};\n\nexport const useBuiCreateQuestion = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: BuiCreateQuestionInput) => {\n const data = await buiQuery<{ buiCreateQuestion: BuiQuestion }>(\n `mutation BuiCreateQuestion($input: BuiCreateQuestionInput!) {\n buiCreateQuestion(input: $input) { ${QUESTION_FIELDS} }\n }`,\n { input },\n baseUrl\n );\n return data.buiCreateQuestion;\n },\n onSuccess: (q) => qc.invalidateQueries({ queryKey: KEY.assessment(q.assessmentId) }),\n });\n};\n\n// ============================================================================\n// Submissions & grading\n// ============================================================================\n\nexport interface UseBuiSubmissionsArgs {\n assessmentId?: string;\n learnerId?: string;\n status?: BuiSubmissionStatus;\n cursor?: string;\n limit?: number;\n enabled?: boolean;\n}\n\nexport const useBuiSubmissions = (args: UseBuiSubmissionsArgs = {}) => {\n const baseUrl = useGateway();\n const { assessmentId, learnerId, status, cursor, limit, enabled = true } = args;\n return useQuery({\n queryKey: [...KEY.submissions, { assessmentId, learnerId, status, cursor, limit }],\n enabled,\n queryFn: async () => {\n const data = await buiQuery<{ buiListSubmissions: BuiConnection<BuiSubmission> }>(\n `query BuiListSubmissions($assessmentId: ID, $learnerId: ID, $status: BuiSubmissionStatus, $pagination: PaginationInput) {\n buiListSubmissions(assessmentId: $assessmentId, learnerId: $learnerId, status: $status, pagination: $pagination) {\n items { ${SUBMISSION_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n { assessmentId, learnerId, status, pagination: pageVars(cursor, limit) },\n baseUrl\n );\n return data.buiListSubmissions;\n },\n });\n};\n\n/** Single-submission lookup — used for deep-linking to `/buildmyiq/submissions/:submissionId`. */\nexport const useBuiSubmission = (id: string | undefined) => {\n const baseUrl = useGateway();\n return useQuery({\n queryKey: id ? KEY.submission(id) : ['buildmyiq', 'submission', 'nil'],\n enabled: Boolean(id),\n queryFn: async () => {\n const data = await buiQuery<{ buiSubmission: BuiSubmission | null }>(\n `query BuiSubmission($id: ID!) {\n buiSubmission(id: $id) { ${SUBMISSION_FIELDS} }\n }`,\n { id },\n baseUrl\n );\n return data.buiSubmission;\n },\n });\n};\n\nexport const useBuiSubmitAssessment = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: BuiSubmitAssessmentInput) => {\n const data = await buiQuery<{ buiSubmitAssessment: BuiSubmission }>(\n `mutation BuiSubmitAssessment($input: BuiSubmitAssessmentInput!) {\n buiSubmitAssessment(input: $input) { ${SUBMISSION_FIELDS} }\n }`,\n { input },\n baseUrl\n );\n return data.buiSubmitAssessment;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.submissions }),\n });\n};\n\nexport const useBuiGradeSubmission = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (args: { id: string; input: BuiGradeSubmissionInput }) => {\n const data = await buiQuery<{ buiGradeSubmission: BuiSubmission }>(\n `mutation BuiGradeSubmission($id: ID!, $input: BuiGradeSubmissionInput!) {\n buiGradeSubmission(id: $id, input: $input) { ${SUBMISSION_FIELDS} }\n }`,\n args,\n baseUrl\n );\n return data.buiGradeSubmission;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.submissions }),\n });\n};\n\n// ============================================================================\n// Enrollments & progress\n// ============================================================================\n\nexport interface UseBuiEnrollmentsArgs {\n courseId?: string;\n learnerId?: string;\n status?: BuiEnrollmentStatus;\n cursor?: string;\n limit?: number;\n enabled?: boolean;\n}\n\nexport const useBuiEnrollments = (args: UseBuiEnrollmentsArgs = {}) => {\n const baseUrl = useGateway();\n const { courseId, learnerId, status, cursor, limit, enabled = true } = args;\n return useQuery({\n queryKey: [...KEY.enrollments, { courseId, learnerId, status, cursor, limit }],\n enabled,\n queryFn: async () => {\n const data = await buiQuery<{ buiListEnrollments: BuiConnection<BuiEnrollment> }>(\n `query BuiListEnrollments($courseId: ID, $learnerId: ID, $status: BuiEnrollmentStatus, $pagination: PaginationInput) {\n buiListEnrollments(courseId: $courseId, learnerId: $learnerId, status: $status, pagination: $pagination) {\n items { ${ENROLLMENT_FIELDS} cohort { ${COHORT_FIELDS} } }\n pageInfo { cursor hasMore }\n }\n }`,\n { courseId, learnerId, status, pagination: pageVars(cursor, limit) },\n baseUrl\n );\n return data.buiListEnrollments;\n },\n });\n};\n\nexport const useBuiEnrollment = (id: string | undefined) => {\n const baseUrl = useGateway();\n return useQuery({\n queryKey: id ? KEY.enrollment(id) : ['buildmyiq', 'enrollment', 'nil'],\n enabled: Boolean(id),\n queryFn: async () => {\n const data = await buiQuery<{ buiEnrollment: BuiEnrollmentWire | null }>(\n `query BuiEnrollment($id: ID!) {\n buiEnrollment(id: $id) { ${ENROLLMENT_FULL_FIELDS} }\n }`,\n { id },\n baseUrl\n );\n return data.buiEnrollment ? flattenEnrollment(data.buiEnrollment) : null;\n },\n });\n};\n\nexport const useBuiEnroll = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: BuiEnrollInput) => {\n const data = await buiQuery<{ buiEnroll: BuiEnrollment }>(\n `mutation BuiEnroll($input: BuiEnrollInput!) {\n buiEnroll(input: $input) { ${ENROLLMENT_FIELDS} }\n }`,\n { input },\n baseUrl\n );\n return data.buiEnroll;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.enrollments }),\n });\n};\n\nexport const useBuiTransitionEnrollment = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (args: { id: string; status: BuiEnrollmentStatus }) => {\n const data = await buiQuery<{ buiTransitionEnrollment: BuiEnrollment }>(\n `mutation BuiTransitionEnrollment($id: ID!, $status: BuiEnrollmentStatus!) {\n buiTransitionEnrollment(id: $id, status: $status) { ${ENROLLMENT_FIELDS} }\n }`,\n args,\n baseUrl\n );\n return data.buiTransitionEnrollment;\n },\n onSuccess: (e) => {\n qc.invalidateQueries({ queryKey: KEY.enrollments });\n qc.invalidateQueries({ queryKey: KEY.enrollment(e.id) });\n },\n });\n};\n\nexport const useBuiRecordLessonProgress = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: BuiRecordLessonProgressInput) => {\n const data = await buiQuery<{ buiRecordLessonProgress: BuiLessonProgress }>(\n `mutation BuiRecordLessonProgress($input: BuiRecordLessonProgressInput!) {\n buiRecordLessonProgress(input: $input) { ${LESSON_PROGRESS_FIELDS} }\n }`,\n { input },\n baseUrl\n );\n return data.buiRecordLessonProgress;\n },\n onSuccess: (p) => {\n qc.invalidateQueries({ queryKey: KEY.enrollment(p.enrollmentId) });\n qc.invalidateQueries({ queryKey: KEY.enrollments });\n },\n });\n};\n\n// ============================================================================\n// Cohorts\n// ============================================================================\n\nexport const useBuiCohorts = (courseId?: string, status?: BuiCohortStatus) => {\n const baseUrl = useGateway();\n return useQuery({\n queryKey: [...KEY.cohorts, { courseId, status }],\n queryFn: async () => {\n const data = await buiQuery<{ buiListCohorts: { items: BuiCohort[] } }>(\n `query BuiListCohorts($courseId: ID, $status: BuiCohortStatus) {\n buiListCohorts(courseId: $courseId, status: $status) { items { ${COHORT_FIELDS} } }\n }`,\n { courseId, status },\n baseUrl\n );\n return data.buiListCohorts.items;\n },\n });\n};\n\nexport const useBuiCreateCohort = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: BuiCreateCohortInput) => {\n const data = await buiQuery<{ buiCreateCohort: BuiCohort }>(\n `mutation BuiCreateCohort($input: BuiCreateCohortInput!) {\n buiCreateCohort(input: $input) { ${COHORT_FIELDS} }\n }`,\n { input },\n baseUrl\n );\n return data.buiCreateCohort;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.cohorts }),\n });\n};\n\n// ============================================================================\n// Skills\n// ============================================================================\n\nexport const useBuiSkills = (category?: BuiSkillCategory) => {\n const baseUrl = useGateway();\n return useQuery({\n queryKey: [...KEY.skills, { category }],\n queryFn: async () => {\n const data = await buiQuery<{ buiListSkills: { items: BuiSkill[] } }>(\n `query BuiListSkills($category: BuiSkillCategory) {\n buiListSkills(category: $category) { items { ${SKILL_FIELDS} } }\n }`,\n { category },\n baseUrl\n );\n return data.buiListSkills.items;\n },\n });\n};\n\nexport const useBuiCreateSkill = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: BuiCreateSkillInput) => {\n const data = await buiQuery<{ buiCreateSkill: BuiSkill }>(\n `mutation BuiCreateSkill($input: BuiCreateSkillInput!) {\n buiCreateSkill(input: $input) { ${SKILL_FIELDS} }\n }`,\n { input },\n baseUrl\n );\n return data.buiCreateSkill;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.skills }),\n });\n};\n\nexport const useBuiSkillStandards = (\n source?: BuiSkillStandard['source'],\n category?: BuiSkillCategory,\n enabled = true\n) => {\n const baseUrl = useGateway();\n return useQuery({\n queryKey: ['buildmyiq', 'skillStandards', { source, category }],\n enabled,\n queryFn: async () => {\n const data = await buiQuery<{ buiListSkillStandards: { items: BuiSkillStandard[] } }>(\n `query BuiListSkillStandards($source: BuiSkillStandardSource, $category: BuiSkillCategory) {\n buiListSkillStandards(source: $source, category: $category) { items { ${SKILL_STANDARD_FIELDS} } }\n }`,\n { source, category },\n baseUrl\n );\n return data.buiListSkillStandards.items;\n },\n });\n};\n\nexport const useBuiCertifications = (body?: BuiCertification['body'], enabled = true) => {\n const baseUrl = useGateway();\n return useQuery({\n queryKey: ['buildmyiq', 'certifications', { body }],\n enabled,\n queryFn: async () => {\n const data = await buiQuery<{ buiListCertifications: { items: BuiCertification[] } }>(\n `query BuiListCertifications($body: BuiCertificationBody) {\n buiListCertifications(body: $body) { items { ${CERTIFICATION_FIELDS} } }\n }`,\n { body },\n baseUrl\n );\n return data.buiListCertifications.items;\n },\n });\n};\n\nexport const useBuiSuggestSkillMappings = (name: string, description?: string, enabled = true) => {\n const baseUrl = useGateway();\n return useQuery({\n queryKey: ['buildmyiq', 'skillMappingSuggestions', { name, description }],\n enabled: enabled && name.trim().length > 0,\n queryFn: async () => {\n const data = await buiQuery<{ buiSuggestSkillMappings: BuiSkillMappingSuggestion[] }>(\n `query BuiSuggestSkillMappings($name: String!, $description: String, $limit: Int) {\n buiSuggestSkillMappings(name: $name, description: $description, limit: $limit) {\n skillStandard { ${SKILL_STANDARD_FIELDS} }\n confidence\n reason\n }\n }`,\n { name, description, limit: 5 },\n baseUrl\n );\n return data.buiSuggestSkillMappings;\n },\n });\n};\n\n// ============================================================================\n// Certificates\n// ============================================================================\n\nexport const useBuiCertificates = (courseId?: string, learnerId?: string) => {\n const baseUrl = useGateway();\n return useQuery({\n queryKey: [...KEY.certificates, { courseId, learnerId }],\n queryFn: async () => {\n const data = await buiQuery<{ buiListCertificates: { items: BuiCertificate[] } }>(\n `query BuiListCertificates($courseId: ID, $learnerId: ID) {\n buiListCertificates(courseId: $courseId, learnerId: $learnerId) { items { ${CERTIFICATE_FIELDS} } }\n }`,\n { courseId, learnerId },\n baseUrl\n );\n return data.buiListCertificates.items;\n },\n });\n};\n\nexport const useBuiIssueCertificate = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: BuiIssueCertificateInput) => {\n const data = await buiQuery<{ buiIssueCertificate: BuiCertificate }>(\n `mutation BuiIssueCertificate($input: BuiIssueCertificateInput!) {\n buiIssueCertificate(input: $input) { ${CERTIFICATE_FIELDS} }\n }`,\n { input },\n baseUrl\n );\n return data.buiIssueCertificate;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.certificates }),\n });\n};\n\nexport const useBuiRevokeCertificate = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (id: string) => {\n const data = await buiQuery<{ buiRevokeCertificate: BuiCertificate }>(\n `mutation BuiRevokeCertificate($id: ID!) {\n buiRevokeCertificate(id: $id) { ${CERTIFICATE_FIELDS} }\n }`,\n { id },\n baseUrl\n );\n return data.buiRevokeCertificate;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.certificates }),\n });\n};\n\n// ============================================================================\n// AI Tutor (BOFF-4426) — grounded lesson Q&A chat\n// ============================================================================\n\n/**\n * The calling learner's own tutor sessions for a lesson (Type-C self-scoped\n * query — no `learnerId` arg on the wire; identity comes from the\n * authenticated actor server-side). Sessions come back newest-first with\n * their full message history, so the panel can restore an in-progress\n * conversation on mount.\n */\nexport const useBuiMyTutorSessions = (lessonId: string | undefined) => {\n const baseUrl = useGateway();\n return useQuery({\n queryKey: lessonId ? KEY.tutorSessions(lessonId) : ['buildmyiq', 'tutorSessions', 'nil'],\n enabled: Boolean(lessonId),\n queryFn: async () => {\n const data = await buiQuery<{ buiMyTutorSessions: { items: BuiTutorSession[] } }>(\n `query BuiMyTutorSessions($lessonId: ID) {\n buiMyTutorSessions(lessonId: $lessonId) { items { ${TUTOR_SESSION_FIELDS} } }\n }`,\n { lessonId },\n baseUrl\n );\n return data.buiMyTutorSessions.items;\n },\n });\n};\n\n/**\n * Ask the AI tutor a question about a lesson. Omit `sessionId` to start a\n * new session for that lesson; pass the current session's id to continue\n * the same conversation (multi-turn). Invalidates the lesson's tutor-session\n * query so the panel picks up the new USER + ASSISTANT turns.\n */\nexport const useBuiTutorAsk = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: BuiTutorAskInput) => {\n const data = await buiQuery<{ buiTutorAsk: BuiTutorAskPayload }>(\n `mutation BuiTutorAsk($input: BuiTutorAskInput!) {\n buiTutorAsk(input: $input) {\n session { ${TUTOR_SESSION_FIELDS} }\n answer { ${TUTOR_MESSAGE_FIELDS} }\n }\n }`,\n { input },\n baseUrl\n );\n return data.buiTutorAsk;\n },\n onSuccess: (payload) => {\n qc.invalidateQueries({ queryKey: KEY.tutorSessions(payload.session.lessonId) });\n },\n });\n};\n\n// ============================================================================\n// AI grading assist + admin copilot + AI course builder (BOFF-3724/4429/4434)\n// ============================================================================\n\nconst GRADE_SUGGESTION_FIELDS = `\n id submissionId proposedScore\n perQuestionFeedback { questionId feedback suggestedPoints }\n rubricRefs confidence inputRefs outcome createdAt\n`;\n\n/**\n * Proposes an AI-assisted score for a submission's open-ended questions —\n * NEVER writes the submission itself. The caller reviews/edits the result\n * and still commits via `useBuiGradeSubmission`. Throws (honest fail-clear)\n * when the backend has no LLM provider configured or the assessment has no\n * open-ended questions.\n */\nexport const useBuiSuggestGrade = () => {\n const baseUrl = useGateway();\n return useMutation({\n mutationFn: async (submissionId: string) => {\n const data = await buiQuery<{ buiSuggestGrade: BuiGradeSuggestion }>(\n `mutation BuiSuggestGrade($submissionId: ID!) {\n buiSuggestGrade(submissionId: $submissionId) { ${GRADE_SUGGESTION_FIELDS} }\n }`,\n { submissionId },\n baseUrl\n );\n return data.buiSuggestGrade;\n },\n });\n};\n\n/** Records whether the educator accepted or rejected a grading suggestion (measurable acceptance rate). */\nexport const useBuiReviewGradeSuggestion = () => {\n const baseUrl = useGateway();\n return useMutation({\n mutationFn: async (args: { id: string; outcome: BuiAiSuggestionOutcome }) => {\n const data = await buiQuery<{ buiReviewGradeSuggestion: BuiGradeSuggestion }>(\n `mutation BuiReviewGradeSuggestion($id: ID!, $outcome: BuiAiSuggestionOutcome!) {\n buiReviewGradeSuggestion(id: $id, outcome: $outcome) { ${GRADE_SUGGESTION_FIELDS} }\n }`,\n args,\n baseUrl\n );\n return data.buiReviewGradeSuggestion;\n },\n });\n};\n\n/**\n * AI-drafted course outline from a topic — an EPHEMERAL suggestion (not\n * persisted server-side); the instructor reviews/edits it before\n * `useBuiApplyCourseOutline` creates anything real.\n */\nexport const useBuiSuggestCourseOutline = () => {\n const baseUrl = useGateway();\n return useMutation({\n mutationFn: async (args: {\n topic: string;\n audienceLevel?: string | null;\n moduleCount?: number | null;\n }) => {\n const data = await buiQuery<{ buiSuggestCourseOutline: BuiCourseOutlineSuggestion }>(\n `mutation BuiSuggestCourseOutline($topic: String!, $audienceLevel: String, $moduleCount: Int) {\n buiSuggestCourseOutline(topic: $topic, audienceLevel: $audienceLevel, moduleCount: $moduleCount) {\n rationale confidence\n modules { title description lessons { title description } }\n }\n }`,\n args,\n baseUrl\n );\n return data.buiSuggestCourseOutline;\n },\n });\n};\n\n/** Creates a real DRAFT course + modules + lessons from a (possibly-edited) AI outline. */\nexport const useBuiApplyCourseOutline = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: BuiApplyCourseOutlineInput) => {\n const data = await buiQuery<{ buiApplyCourseOutline: BuiCourse }>(\n `mutation BuiApplyCourseOutline($input: BuiApplyCourseOutlineInput!) {\n buiApplyCourseOutline(input: $input) { ${COURSE_CORE_FIELDS} }\n }`,\n { input },\n baseUrl\n );\n return data.buiApplyCourseOutline;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.courses }),\n });\n};\n\n/**\n * Read-only admin Q&A grounded in real workspace stats — NEVER executes an\n * admin action. Stateless per call; the caller resends prior turns as\n * `history` for multi-turn context.\n */\nexport const useBuiAdminCopilotAsk = () => {\n const baseUrl = useGateway();\n return useMutation({\n mutationFn: async (args: { question: string; history: BuiAdminCopilotTurnInput[] }) => {\n const data = await buiQuery<{ buiAdminCopilotAsk: BuiAdminCopilotAnswer }>(\n `mutation BuiAdminCopilotAsk($question: String!, $history: [BuiAdminCopilotTurnInput!]) {\n buiAdminCopilotAsk(question: $question, history: $history) {\n answer contextCourseIds generatedAt\n }\n }`,\n args,\n baseUrl\n );\n return data.buiAdminCopilotAsk;\n },\n });\n};\n\n// ============================================================================\n// LTI 1.3 Advantage — platform registry (BOFF-4419)\n// ============================================================================\n\nconst LTI_PLATFORM_FIELDS = `\n id workspaceId name issuer clientId deploymentIds\n authLoginUrl authTokenUrl keySetUrl active createdAt updatedAt\n`;\n\nexport const useBuiLtiPlatforms = () => {\n const baseUrl = useGateway();\n return useQuery({\n queryKey: KEY.ltiPlatforms,\n queryFn: async () => {\n const data = await buiQuery<{ buiLtiPlatforms: BuiLtiPlatform[] }>(\n `query BuiLtiPlatforms { buiLtiPlatforms { ${LTI_PLATFORM_FIELDS} } }`,\n {},\n baseUrl\n );\n return data.buiLtiPlatforms;\n },\n });\n};\n\nexport const useBuiRegisterLtiPlatform = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: BuiRegisterLtiPlatformInput) => {\n const data = await buiQuery<{ buiRegisterLtiPlatform: BuiLtiPlatform }>(\n `mutation BuiRegisterLtiPlatform($input: BuiRegisterLtiPlatformInput!) {\n buiRegisterLtiPlatform(input: $input) { ${LTI_PLATFORM_FIELDS} }\n }`,\n { input },\n baseUrl\n );\n return data.buiRegisterLtiPlatform;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.ltiPlatforms }),\n });\n};\n\nexport const useBuiDeactivateLtiPlatform = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (id: string) => {\n const data = await buiQuery<{ buiDeactivateLtiPlatform: boolean }>(\n `mutation BuiDeactivateLtiPlatform($id: ID!) { buiDeactivateLtiPlatform(id: $id) }`,\n { id },\n baseUrl\n );\n return data.buiDeactivateLtiPlatform;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.ltiPlatforms }),\n });\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"useBuildmyiqApi.js","names":[],"sources":["../../../src/buildmyiq/hooks/useBuildmyiqApi.ts"],"sourcesContent":["/**\n * Buildmyiq API hooks\n * @module buildmyiq/hooks\n *\n * React-Query hooks wired DIRECTLY to the real `wspace-buildmyiq-svc`\n * federated GraphQL contract (the `bui*` operations). Every query/mutation\n * below uses an inline gql string whose fields match the svc\n * `.hive-schema.graphql` exactly. No mock data, no codegen.\n *\n * Transport: `graphqlFetch({ gateway: 'workspace', workspaceToken: true })` —\n * the host shell supplies the gateway base URL + a workspace-scoped token.\n */\n\nimport { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';\nimport { graphqlFetch } from '@burdenoff/fe-libs/shared/graphql';\n\nimport { useBuildmyiq } from '../context';\nimport type {\n BuiConnection,\n BuiCourse,\n BuiCourseLevel,\n BuiCourseStatus,\n BuiModule,\n BuiLesson,\n BuiAssessment,\n BuiAssessmentStatus,\n BuiQuestion,\n BuiSubmission,\n BuiSubmissionStatus,\n BuiEnrollment,\n BuiEnrollmentStatus,\n BuiCohort,\n BuiCohortStatus,\n BuiSkill,\n BuiSkillCategory,\n BuiSkillStandard,\n BuiCertification,\n BuiSkillMappingSuggestion,\n BuiCertificate,\n BuiLessonProgress,\n BuiCreateCourseInput,\n BuiUpdateCourseInput,\n BuiCreateModuleInput,\n BuiCreateLessonInput,\n BuiCreateAssessmentInput,\n BuiCreateQuestionInput,\n BuiCreateSkillInput,\n BuiCreateCohortInput,\n BuiEnrollInput,\n BuiRecordLessonProgressInput,\n BuiSubmitAssessmentInput,\n BuiGradeSubmissionInput,\n BuiIssueCertificateInput,\n BuiTutorAskInput,\n BuiTutorAskPayload,\n BuiTutorSession,\n BuiGradeSuggestion,\n BuiCourseOutlineSuggestion,\n BuiApplyCourseOutlineInput,\n BuiAdminCopilotAnswer,\n BuiAdminCopilotTurnInput,\n BuiAiSuggestionOutcome,\n BuiLtiPlatform,\n BuiRegisterLtiPlatformInput,\n PaginationInput,\n} from '../types';\n\n// ============================================================================\n// Transport\n// ============================================================================\n\n/**\n * fe-libs `graphqlFetch` coalesces `query` responses in an in-memory cache keyed\n * by operationName+variables for ~15s (mutations are never cached). A read\n * issued right after a write (e.g. the React-Query refetch triggered by our\n * `invalidateQueries`) would otherwise return the PRE-mutation response until the\n * TTL expires — making just-created/updated rows invisible until a full reload.\n * We clear those caches after every buildmyiq mutation so every follow-up read\n * is fresh.\n */\ntype GraphqlReadCacheGlobals = {\n __graphqlFetchResponseCache?: { clear: () => void };\n __graphqlFetchPendingResponses?: { clear: () => void };\n __boffGraphQLResponseCache?: { clear: () => void };\n};\n\nconst clearGraphqlReadCaches = (): void => {\n const g = globalThis as GraphqlReadCacheGlobals;\n g.__graphqlFetchResponseCache?.clear();\n g.__graphqlFetchPendingResponses?.clear();\n g.__boffGraphQLResponseCache?.clear();\n};\n\nconst buiQuery = async <TData>(\n query: string,\n variables: Record<string, unknown>,\n baseUrl?: string\n): Promise<TData> => {\n const response = await graphqlFetch<TData>({\n gateway: 'workspace',\n baseUrl,\n query,\n variables,\n workspaceToken: true,\n });\n\n if (response.errors?.length) {\n throw new Error(response.errors[0]?.message ?? 'GraphQL request failed');\n }\n if (!response.data) {\n throw new Error('GraphQL response did not include data');\n }\n if (/^\\s*mutation\\b/.test(query)) {\n clearGraphqlReadCaches();\n }\n return response.data;\n};\n\n// React-Query key roots (one namespace per domain entity).\nconst KEY = {\n courses: ['buildmyiq', 'courses'] as const,\n course: (id: string) => ['buildmyiq', 'course', id] as const,\n modules: (courseId: string) => ['buildmyiq', 'modules', courseId] as const,\n lessons: (courseId: string) => ['buildmyiq', 'lessons', courseId] as const,\n assessments: (courseId: string) => ['buildmyiq', 'assessments', courseId] as const,\n assessment: (id: string) => ['buildmyiq', 'assessment', id] as const,\n enrollments: ['buildmyiq', 'enrollments'] as const,\n enrollment: (id: string) => ['buildmyiq', 'enrollment', id] as const,\n certificates: ['buildmyiq', 'certificates'] as const,\n cohorts: ['buildmyiq', 'cohorts'] as const,\n skills: ['buildmyiq', 'skills'] as const,\n submissions: ['buildmyiq', 'submissions'] as const,\n submission: (id: string) => ['buildmyiq', 'submission', id] as const,\n tutorSessions: (lessonId: string) => ['buildmyiq', 'tutorSessions', lessonId] as const,\n ltiPlatforms: ['buildmyiq', 'ltiPlatforms'] as const,\n};\n\nconst DEFAULT_PAGE_SIZE = 24;\n\n/**\n * Build a `PaginationInput` for a list query. The FIRST page (undefined cursor)\n * OMITS the cursor key entirely — the svc rejects an explicit `cursor: null`.\n */\nconst pageVars = (cursor?: string, limit = DEFAULT_PAGE_SIZE): PaginationInput =>\n cursor ? { limit, cursor } : { limit };\n\n// ============================================================================\n// Reusable gql field selections (codegen-free strictness)\n// ============================================================================\n\nconst FILE_FIELDS = `id name downloadUrl mimeType`;\n\nconst SKILL_FIELDS = `\n id workspaceId name category description isActive\n onetCode escoUri socCodes certificationIds taxonomyMetadata\n createdAt updatedAt\n`;\n\nconst SKILL_STANDARD_FIELDS = `\n id workspaceId source code title description category uri socCodes metadata isActive createdAt updatedAt\n`;\n\nconst CERTIFICATION_FIELDS = `\n id workspaceId body code name description url isActive createdAt updatedAt\n`;\n\nconst COURSE_CORE_FIELDS = `\n id workspaceId title slug summary description status level language\n estimatedHours isPublic tags skillId instructorId publishedAt createdAt updatedAt\n cover { ${FILE_FIELDS} }\n skill { id name category }\n`;\n\nconst MODULE_FIELDS = `\n id workspaceId courseId title description position createdAt updatedAt\n`;\n\nconst LESSON_FIELDS = `\n id workspaceId courseId moduleId title type content durationMin position isPreview\n createdAt updatedAt\n media { ${FILE_FIELDS} }\n`;\n\n// NOTE: `modules`/`lessons` are `Bui*Connection!` on the svc schema (paginated),\n// NOT plain lists — every nested selection below must go through `items { }` or\n// the gateway rejects the whole query with GRAPHQL_VALIDATION_FAILED (BOFF-4703\n// Connection-shape drift). See `flattenCourse` for the matching unwrap.\nconst COURSE_FULL_FIELDS = `\n ${COURSE_CORE_FIELDS}\n modules { items { ${MODULE_FIELDS} lessons { items { ${LESSON_FIELDS} } } } }\n lessons { items { ${LESSON_FIELDS} } }\n`;\n\nconst ASSESSMENT_FIELDS = `\n id workspaceId courseId title description type status passingScore maxAttempts\n timeLimitMin isGraded createdAt updatedAt\n`;\n\n// NOTE: BuiQuestion has NO `answerKey` on the type (create-input only).\nconst QUESTION_FIELDS = `\n id workspaceId assessmentId prompt type options points position explanation\n createdAt updatedAt\n`;\n\n// `questions` is `BuiQuestionConnection!` — see COURSE_FULL_FIELDS note above.\nconst ASSESSMENT_FULL_FIELDS = `\n ${ASSESSMENT_FIELDS}\n questions { items { ${QUESTION_FIELDS} } }\n`;\n\nconst SUBMISSION_FIELDS = `\n id workspaceId assessmentId enrollmentId learnerId attempt answers status score\n passed feedback gradedById gradedAt submittedAt createdAt updatedAt\n`;\n\nconst COHORT_FIELDS = `\n id workspaceId courseId name capacity facilitatorId startDate endDate status\n createdAt updatedAt\n`;\n\nconst LESSON_PROGRESS_FIELDS = `\n id workspaceId enrollmentId lessonId status secondsSpent completedAt createdAt updatedAt\n`;\n\nconst ENROLLMENT_FIELDS = `\n id workspaceId courseId learnerId cohortId status progressPct enrolledAt startedAt\n completedAt lastActiveAt expiresAt createdAt updatedAt\n`;\n\n// `lessonProgress` is `BuiLessonProgressConnection!` — see COURSE_FULL_FIELDS note above.\nconst ENROLLMENT_FULL_FIELDS = `\n ${ENROLLMENT_FIELDS}\n cohort { ${COHORT_FIELDS} }\n lessonProgress { items { ${LESSON_PROGRESS_FIELDS} } }\n`;\n\nconst CERTIFICATE_FIELDS = `\n id workspaceId courseId learnerId enrollmentId serial status issuedAt expiresAt\n revokedAt finalScore createdAt updatedAt\n file { ${FILE_FIELDS} }\n`;\n\nconst TUTOR_MESSAGE_FIELDS = `\n id workspaceId sessionId role content groundingLessonIds adaptiveLevel createdAt\n`;\n\nconst TUTOR_SESSION_FIELDS = `\n id workspaceId courseId lessonId learnerId createdAt updatedAt\n messages { ${TUTOR_MESSAGE_FIELDS} }\n`;\n\n// ============================================================================\n// Connection-shape unwrap (BOFF-4703)\n// ============================================================================\n// Every `Bui*Connection` field on the wire is `{ items: T[]; pageInfo }`, never\n// a bare list — including NESTED fields like `BuiCourse.modules` or\n// `BuiAssessment.questions`. The hooks below keep returning plain flat arrays\n// to consumers (that's the shape every page in this MFE is written against),\n// so raw wire responses are unwrapped once here rather than pushing the\n// Connection shape out to every page.\n\n/** Wire shape of a `BuiModule` fetched via `COURSE_FULL_FIELDS` (nested `lessons` is a Connection). */\ntype BuiModuleWire = Omit<BuiModule, 'lessons'> & { lessons: { items: BuiLesson[] } };\n\n/** Wire shape of a `BuiCourse` fetched via `COURSE_FULL_FIELDS`. */\ntype BuiCourseWire = Omit<BuiCourse, 'modules' | 'lessons'> & {\n modules: { items: BuiModuleWire[] };\n lessons: { items: BuiLesson[] };\n};\n\nconst flattenCourse = (c: BuiCourseWire): BuiCourse => ({\n ...c,\n modules: c.modules.items.map((m) => ({ ...m, lessons: m.lessons.items })),\n lessons: c.lessons.items,\n});\n\n/** Wire shape of a `BuiAssessment` fetched via `ASSESSMENT_FULL_FIELDS`. */\ntype BuiAssessmentWire = Omit<BuiAssessment, 'questions'> & { questions: { items: BuiQuestion[] } };\n\nconst flattenAssessment = (a: BuiAssessmentWire): BuiAssessment => ({\n ...a,\n questions: a.questions.items,\n});\n\n/** Wire shape of a `BuiEnrollment` fetched via `ENROLLMENT_FULL_FIELDS`. */\ntype BuiEnrollmentWire = Omit<BuiEnrollment, 'lessonProgress'> & {\n lessonProgress: { items: BuiLessonProgress[] };\n};\n\nconst flattenEnrollment = (e: BuiEnrollmentWire): BuiEnrollment => ({\n ...e,\n lessonProgress: e.lessonProgress.items,\n});\n\n// ============================================================================\n// Shared hook helper: resolve gateway URL from context\n// ============================================================================\n\nconst useGateway = (): string | undefined => {\n const { apiGatewayUrl } = useBuildmyiq();\n return apiGatewayUrl;\n};\n\n// ============================================================================\n// Courses\n// ============================================================================\n\nexport interface UseBuiCoursesArgs {\n level?: BuiCourseLevel;\n status?: BuiCourseStatus;\n cursor?: string;\n limit?: number;\n enabled?: boolean;\n}\n\nexport const useBuiCourses = (args: UseBuiCoursesArgs = {}) => {\n const baseUrl = useGateway();\n const { level, status, cursor, limit, enabled = true } = args;\n return useQuery({\n queryKey: [...KEY.courses, { level, status, cursor, limit }],\n enabled,\n queryFn: async () => {\n // Only `id` is needed here (list view shows module/lesson COUNTS, not\n // full records — see CourseCard.tsx) so we intentionally under-select\n // vs. the full `BuiModule`/`BuiLesson` shape; still must go through\n // `items { }` since both fields are Connections (BOFF-4703).\n const data = await buiQuery<{\n buiListCourses: {\n items: Array<\n Omit<BuiCourse, 'modules' | 'lessons'> & {\n modules: { items: Array<{ id: string }> };\n lessons: { items: Array<{ id: string }> };\n }\n >;\n pageInfo: BuiConnection<BuiCourse>['pageInfo'];\n };\n }>(\n `query BuiListCourses($level: BuiCourseLevel, $status: BuiCourseStatus, $pagination: PaginationInput) {\n buiListCourses(level: $level, status: $status, pagination: $pagination) {\n items { ${COURSE_CORE_FIELDS} modules { items { id } } lessons { items { id } } }\n pageInfo { cursor hasMore }\n }\n }`,\n { level, status, pagination: pageVars(cursor, limit) },\n baseUrl\n );\n return {\n items: data.buiListCourses.items.map((c) => ({\n ...c,\n // Partial `{ id }`-only records are cast to the full type — safe here\n // because every consumer of the list view only reads `.length`.\n modules: c.modules.items as unknown as BuiModule[],\n lessons: c.lessons.items as unknown as BuiLesson[],\n })),\n pageInfo: data.buiListCourses.pageInfo,\n };\n },\n });\n};\n\nexport const useBuiCourse = (id: string | undefined) => {\n const baseUrl = useGateway();\n return useQuery({\n queryKey: id ? KEY.course(id) : ['buildmyiq', 'course', 'nil'],\n enabled: Boolean(id),\n queryFn: async () => {\n const data = await buiQuery<{ buiCourse: BuiCourseWire | null }>(\n `query BuiCourse($id: ID!) {\n buiCourse(id: $id) { ${COURSE_FULL_FIELDS} }\n }`,\n { id },\n baseUrl\n );\n return data.buiCourse ? flattenCourse(data.buiCourse) : null;\n },\n });\n};\n\nexport const useBuiCreateCourse = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: BuiCreateCourseInput) => {\n const data = await buiQuery<{ buiCreateCourse: BuiCourse }>(\n `mutation BuiCreateCourse($input: BuiCreateCourseInput!) {\n buiCreateCourse(input: $input) { ${COURSE_CORE_FIELDS} }\n }`,\n { input },\n baseUrl\n );\n return data.buiCreateCourse;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.courses }),\n });\n};\n\nexport const useBuiUpdateCourse = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (args: { id: string; input: BuiUpdateCourseInput }) => {\n const data = await buiQuery<{ buiUpdateCourse: BuiCourse }>(\n `mutation BuiUpdateCourse($id: ID!, $input: BuiUpdateCourseInput!) {\n buiUpdateCourse(id: $id, input: $input) { ${COURSE_CORE_FIELDS} }\n }`,\n args,\n baseUrl\n );\n return data.buiUpdateCourse;\n },\n onSuccess: (course) => {\n qc.invalidateQueries({ queryKey: KEY.courses });\n qc.invalidateQueries({ queryKey: KEY.course(course.id) });\n },\n });\n};\n\nexport const useBuiPublishCourse = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (id: string) => {\n const data = await buiQuery<{ buiPublishCourse: BuiCourse }>(\n `mutation BuiPublishCourse($id: ID!) {\n buiPublishCourse(id: $id) { ${COURSE_CORE_FIELDS} }\n }`,\n { id },\n baseUrl\n );\n return data.buiPublishCourse;\n },\n onSuccess: (course) => {\n qc.invalidateQueries({ queryKey: KEY.courses });\n qc.invalidateQueries({ queryKey: KEY.course(course.id) });\n },\n });\n};\n\nexport const useBuiArchiveCourse = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (id: string) => {\n const data = await buiQuery<{ buiArchiveCourse: boolean }>(\n `mutation BuiArchiveCourse($id: ID!) { buiArchiveCourse(id: $id) }`,\n { id },\n baseUrl\n );\n return data.buiArchiveCourse;\n },\n onSuccess: (_r, id) => {\n qc.invalidateQueries({ queryKey: KEY.courses });\n qc.invalidateQueries({ queryKey: KEY.course(id) });\n },\n });\n};\n\n// ============================================================================\n// Modules & lessons\n// ============================================================================\n\nexport const useBuiModules = (courseId: string | undefined) => {\n const baseUrl = useGateway();\n return useQuery({\n queryKey: courseId ? KEY.modules(courseId) : ['buildmyiq', 'modules', 'nil'],\n enabled: Boolean(courseId),\n queryFn: async () => {\n const data = await buiQuery<{ buiListModules: { items: BuiModuleWire[] } }>(\n `query BuiListModules($courseId: ID!) {\n buiListModules(courseId: $courseId) {\n items { ${MODULE_FIELDS} lessons { items { ${LESSON_FIELDS} } } }\n }\n }`,\n { courseId },\n baseUrl\n );\n return data.buiListModules.items.map((m) => ({ ...m, lessons: m.lessons.items }));\n },\n });\n};\n\nexport const useBuiLessons = (courseId: string | undefined, moduleId?: string) => {\n const baseUrl = useGateway();\n return useQuery({\n queryKey: courseId ? [...KEY.lessons(courseId), { moduleId }] : ['buildmyiq', 'lessons', 'nil'],\n enabled: Boolean(courseId),\n queryFn: async () => {\n const data = await buiQuery<{ buiListLessons: { items: BuiLesson[] } }>(\n `query BuiListLessons($courseId: ID!, $moduleId: ID) {\n buiListLessons(courseId: $courseId, moduleId: $moduleId) { items { ${LESSON_FIELDS} } }\n }`,\n { courseId, moduleId },\n baseUrl\n );\n return data.buiListLessons.items;\n },\n });\n};\n\nexport const useBuiCreateModule = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: BuiCreateModuleInput) => {\n const data = await buiQuery<{ buiCreateModule: BuiModule }>(\n `mutation BuiCreateModule($input: BuiCreateModuleInput!) {\n buiCreateModule(input: $input) { ${MODULE_FIELDS} }\n }`,\n { input },\n baseUrl\n );\n return data.buiCreateModule;\n },\n onSuccess: (m) => {\n qc.invalidateQueries({ queryKey: KEY.modules(m.courseId) });\n qc.invalidateQueries({ queryKey: KEY.course(m.courseId) });\n },\n });\n};\n\nexport const useBuiCreateLesson = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: BuiCreateLessonInput) => {\n const data = await buiQuery<{ buiCreateLesson: BuiLesson }>(\n `mutation BuiCreateLesson($input: BuiCreateLessonInput!) {\n buiCreateLesson(input: $input) { ${LESSON_FIELDS} }\n }`,\n { input },\n baseUrl\n );\n return data.buiCreateLesson;\n },\n onSuccess: (l) => {\n qc.invalidateQueries({ queryKey: KEY.lessons(l.courseId) });\n qc.invalidateQueries({ queryKey: KEY.modules(l.courseId) });\n qc.invalidateQueries({ queryKey: KEY.course(l.courseId) });\n },\n });\n};\n\n// ============================================================================\n// Assessments & questions\n// ============================================================================\n\nexport const useBuiAssessments = (courseId: string | undefined, status?: BuiAssessmentStatus) => {\n const baseUrl = useGateway();\n return useQuery({\n queryKey: courseId\n ? [...KEY.assessments(courseId), { status }]\n : ['buildmyiq', 'assessments', 'nil'],\n enabled: Boolean(courseId),\n queryFn: async () => {\n // Only `id` is needed here (list view shows a question COUNT, not full\n // records — see AssessmentsPage.tsx); still must go through `items { }`\n // since `questions` is a Connection (BOFF-4703).\n const data = await buiQuery<{\n buiListAssessments: {\n items: Array<\n Omit<BuiAssessment, 'questions'> & { questions: { items: Array<{ id: string }> } }\n >;\n };\n }>(\n `query BuiListAssessments($courseId: ID!, $status: BuiAssessmentStatus) {\n buiListAssessments(courseId: $courseId, status: $status) {\n items { ${ASSESSMENT_FIELDS} questions { items { id } } }\n }\n }`,\n { courseId, status },\n baseUrl\n );\n return data.buiListAssessments.items.map((a) => ({\n ...a,\n // Partial `{ id }`-only records are cast to the full type — safe here\n // because every consumer of the list view only reads `.length`.\n questions: a.questions.items as unknown as BuiQuestion[],\n }));\n },\n });\n};\n\nexport const useBuiAssessment = (id: string | undefined) => {\n const baseUrl = useGateway();\n return useQuery({\n queryKey: id ? KEY.assessment(id) : ['buildmyiq', 'assessment', 'nil'],\n enabled: Boolean(id),\n queryFn: async () => {\n const data = await buiQuery<{ buiAssessment: BuiAssessmentWire | null }>(\n `query BuiAssessment($id: ID!) {\n buiAssessment(id: $id) { ${ASSESSMENT_FULL_FIELDS} }\n }`,\n { id },\n baseUrl\n );\n return data.buiAssessment ? flattenAssessment(data.buiAssessment) : null;\n },\n });\n};\n\nexport const useBuiCreateAssessment = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: BuiCreateAssessmentInput) => {\n const data = await buiQuery<{ buiCreateAssessment: BuiAssessment }>(\n `mutation BuiCreateAssessment($input: BuiCreateAssessmentInput!) {\n buiCreateAssessment(input: $input) { ${ASSESSMENT_FIELDS} }\n }`,\n { input },\n baseUrl\n );\n return data.buiCreateAssessment;\n },\n onSuccess: (a) => qc.invalidateQueries({ queryKey: KEY.assessments(a.courseId) }),\n });\n};\n\nexport const useBuiPublishAssessment = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (id: string) => {\n const data = await buiQuery<{ buiPublishAssessment: BuiAssessment }>(\n `mutation BuiPublishAssessment($id: ID!) {\n buiPublishAssessment(id: $id) { ${ASSESSMENT_FIELDS} }\n }`,\n { id },\n baseUrl\n );\n return data.buiPublishAssessment;\n },\n onSuccess: (a) => {\n qc.invalidateQueries({ queryKey: KEY.assessment(a.id) });\n qc.invalidateQueries({ queryKey: KEY.assessments(a.courseId) });\n },\n });\n};\n\n/**\n * Archive an assessment (soft-delete) — releases its reusable\n * `buildmyiq.assessments.count` quota slot server-side (BOFF-4722) so the\n * workspace can create a replacement without hitting the plan limit.\n * `buiArchiveAssessment` returns only a `Boolean!`, not the record, so the\n * caller supplies `courseId` (already in hand from the loaded assessment)\n * to invalidate the right list query.\n */\nexport const useBuiArchiveAssessment = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (args: { id: string; courseId: string }) => {\n const data = await buiQuery<{ buiArchiveAssessment: boolean }>(\n `mutation BuiArchiveAssessment($id: ID!) { buiArchiveAssessment(id: $id) }`,\n { id: args.id },\n baseUrl\n );\n return data.buiArchiveAssessment;\n },\n onSuccess: (_r, args) => {\n qc.invalidateQueries({ queryKey: KEY.assessment(args.id) });\n qc.invalidateQueries({ queryKey: KEY.assessments(args.courseId) });\n },\n });\n};\n\nexport const useBuiCreateQuestion = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: BuiCreateQuestionInput) => {\n const data = await buiQuery<{ buiCreateQuestion: BuiQuestion }>(\n `mutation BuiCreateQuestion($input: BuiCreateQuestionInput!) {\n buiCreateQuestion(input: $input) { ${QUESTION_FIELDS} }\n }`,\n { input },\n baseUrl\n );\n return data.buiCreateQuestion;\n },\n onSuccess: (q) => qc.invalidateQueries({ queryKey: KEY.assessment(q.assessmentId) }),\n });\n};\n\n// ============================================================================\n// Submissions & grading\n// ============================================================================\n\nexport interface UseBuiSubmissionsArgs {\n assessmentId?: string;\n status?: BuiSubmissionStatus;\n cursor?: string;\n limit?: number;\n enabled?: boolean;\n}\n\nexport const useBuiSubmissions = (args: UseBuiSubmissionsArgs = {}) => {\n const baseUrl = useGateway();\n const { assessmentId, status, cursor, limit, enabled = true } = args;\n return useQuery({\n queryKey: [...KEY.submissions, { assessmentId, status, cursor, limit }],\n enabled,\n queryFn: async () => {\n const data = await buiQuery<{ buiListSubmissions: BuiConnection<BuiSubmission> }>(\n `query BuiListSubmissions($assessmentId: ID, $status: BuiSubmissionStatus, $pagination: PaginationInput) {\n buiListSubmissions(assessmentId: $assessmentId, status: $status, pagination: $pagination) {\n items { ${SUBMISSION_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n { assessmentId, status, pagination: pageVars(cursor, limit) },\n baseUrl\n );\n return data.buiListSubmissions;\n },\n });\n};\n\n/**\n * The signed-in learner's own submissions (self-scoped). The backend dropped\n * the `learnerId` arg from `buiListSubmissions` in the FERPA/IDOR hardening;\n * a learner's own results come from `buiMySubmissions` instead.\n */\nexport const useBuiMySubmissions = (args: UseBuiSubmissionsArgs = {}) => {\n const baseUrl = useGateway();\n const { assessmentId, status, cursor, limit, enabled = true } = args;\n return useQuery({\n queryKey: [...KEY.submissions, 'mine', { assessmentId, status, cursor, limit }],\n enabled,\n queryFn: async () => {\n const data = await buiQuery<{ buiMySubmissions: BuiConnection<BuiSubmission> }>(\n `query BuiMySubmissions($assessmentId: ID, $status: BuiSubmissionStatus, $pagination: PaginationInput) {\n buiMySubmissions(assessmentId: $assessmentId, status: $status, pagination: $pagination) {\n items { ${SUBMISSION_FIELDS} }\n pageInfo { cursor hasMore }\n }\n }`,\n { assessmentId, status, pagination: pageVars(cursor, limit) },\n baseUrl\n );\n return data.buiMySubmissions;\n },\n });\n};\n\n/** Single-submission lookup — used for deep-linking to `/buildmyiq/submissions/:submissionId`. */\nexport const useBuiSubmission = (id: string | undefined) => {\n const baseUrl = useGateway();\n return useQuery({\n queryKey: id ? KEY.submission(id) : ['buildmyiq', 'submission', 'nil'],\n enabled: Boolean(id),\n queryFn: async () => {\n const data = await buiQuery<{ buiSubmission: BuiSubmission | null }>(\n `query BuiSubmission($id: ID!) {\n buiSubmission(id: $id) { ${SUBMISSION_FIELDS} }\n }`,\n { id },\n baseUrl\n );\n return data.buiSubmission;\n },\n });\n};\n\nexport const useBuiSubmitAssessment = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: BuiSubmitAssessmentInput) => {\n const data = await buiQuery<{ buiSubmitAssessment: BuiSubmission }>(\n `mutation BuiSubmitAssessment($input: BuiSubmitAssessmentInput!) {\n buiSubmitAssessment(input: $input) { ${SUBMISSION_FIELDS} }\n }`,\n { input },\n baseUrl\n );\n return data.buiSubmitAssessment;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.submissions }),\n });\n};\n\nexport const useBuiGradeSubmission = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (args: { id: string; input: BuiGradeSubmissionInput }) => {\n const data = await buiQuery<{ buiGradeSubmission: BuiSubmission }>(\n `mutation BuiGradeSubmission($id: ID!, $input: BuiGradeSubmissionInput!) {\n buiGradeSubmission(id: $id, input: $input) { ${SUBMISSION_FIELDS} }\n }`,\n args,\n baseUrl\n );\n return data.buiGradeSubmission;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.submissions }),\n });\n};\n\n// ============================================================================\n// Enrollments & progress\n// ============================================================================\n\nexport interface UseBuiEnrollmentsArgs {\n courseId?: string;\n status?: BuiEnrollmentStatus;\n cursor?: string;\n limit?: number;\n enabled?: boolean;\n}\n\n/**\n * Workspace-wide enrollment list (instructor/admin). The backend dropped the\n * `learnerId` arg in the FERPA/IDOR hardening — a learner's OWN enrollments\n * come from the self-scoped `useBuiMyEnrollments` instead.\n */\nexport const useBuiEnrollments = (args: UseBuiEnrollmentsArgs = {}) => {\n const baseUrl = useGateway();\n const { courseId, status, cursor, limit, enabled = true } = args;\n return useQuery({\n queryKey: [...KEY.enrollments, { courseId, status, cursor, limit }],\n enabled,\n queryFn: async () => {\n const data = await buiQuery<{ buiListEnrollments: BuiConnection<BuiEnrollment> }>(\n `query BuiListEnrollments($courseId: ID, $status: BuiEnrollmentStatus, $pagination: PaginationInput) {\n buiListEnrollments(courseId: $courseId, status: $status, pagination: $pagination) {\n items { ${ENROLLMENT_FIELDS} cohort { ${COHORT_FIELDS} } }\n pageInfo { cursor hasMore }\n }\n }`,\n { courseId, status, pagination: pageVars(cursor, limit) },\n baseUrl\n );\n return data.buiListEnrollments;\n },\n });\n};\n\n/** The signed-in learner's own enrollments (self-scoped, no learnerId arg). */\nexport const useBuiMyEnrollments = (args: UseBuiEnrollmentsArgs = {}) => {\n const baseUrl = useGateway();\n const { courseId, status, cursor, limit, enabled = true } = args;\n return useQuery({\n queryKey: [...KEY.enrollments, 'mine', { courseId, status, cursor, limit }],\n enabled,\n queryFn: async () => {\n const data = await buiQuery<{ buiMyEnrollments: BuiConnection<BuiEnrollment> }>(\n `query BuiMyEnrollments($courseId: ID, $status: BuiEnrollmentStatus, $pagination: PaginationInput) {\n buiMyEnrollments(courseId: $courseId, status: $status, pagination: $pagination) {\n items { ${ENROLLMENT_FIELDS} cohort { ${COHORT_FIELDS} } }\n pageInfo { cursor hasMore }\n }\n }`,\n { courseId, status, pagination: pageVars(cursor, limit) },\n baseUrl\n );\n return data.buiMyEnrollments;\n },\n });\n};\n\nexport const useBuiEnrollment = (id: string | undefined) => {\n const baseUrl = useGateway();\n return useQuery({\n queryKey: id ? KEY.enrollment(id) : ['buildmyiq', 'enrollment', 'nil'],\n enabled: Boolean(id),\n queryFn: async () => {\n const data = await buiQuery<{ buiEnrollment: BuiEnrollmentWire | null }>(\n `query BuiEnrollment($id: ID!) {\n buiEnrollment(id: $id) { ${ENROLLMENT_FULL_FIELDS} }\n }`,\n { id },\n baseUrl\n );\n return data.buiEnrollment ? flattenEnrollment(data.buiEnrollment) : null;\n },\n });\n};\n\nexport const useBuiEnroll = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: BuiEnrollInput) => {\n const data = await buiQuery<{ buiEnroll: BuiEnrollment }>(\n `mutation BuiEnroll($input: BuiEnrollInput!) {\n buiEnroll(input: $input) { ${ENROLLMENT_FIELDS} }\n }`,\n { input },\n baseUrl\n );\n return data.buiEnroll;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.enrollments }),\n });\n};\n\nexport const useBuiTransitionEnrollment = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (args: { id: string; status: BuiEnrollmentStatus }) => {\n const data = await buiQuery<{ buiTransitionEnrollment: BuiEnrollment }>(\n `mutation BuiTransitionEnrollment($id: ID!, $status: BuiEnrollmentStatus!) {\n buiTransitionEnrollment(id: $id, status: $status) { ${ENROLLMENT_FIELDS} }\n }`,\n args,\n baseUrl\n );\n return data.buiTransitionEnrollment;\n },\n onSuccess: (e) => {\n qc.invalidateQueries({ queryKey: KEY.enrollments });\n qc.invalidateQueries({ queryKey: KEY.enrollment(e.id) });\n },\n });\n};\n\nexport const useBuiRecordLessonProgress = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: BuiRecordLessonProgressInput) => {\n const data = await buiQuery<{ buiRecordLessonProgress: BuiLessonProgress }>(\n `mutation BuiRecordLessonProgress($input: BuiRecordLessonProgressInput!) {\n buiRecordLessonProgress(input: $input) { ${LESSON_PROGRESS_FIELDS} }\n }`,\n { input },\n baseUrl\n );\n return data.buiRecordLessonProgress;\n },\n onSuccess: (p) => {\n qc.invalidateQueries({ queryKey: KEY.enrollment(p.enrollmentId) });\n qc.invalidateQueries({ queryKey: KEY.enrollments });\n },\n });\n};\n\n// ============================================================================\n// Cohorts\n// ============================================================================\n\nexport const useBuiCohorts = (courseId?: string, status?: BuiCohortStatus) => {\n const baseUrl = useGateway();\n return useQuery({\n queryKey: [...KEY.cohorts, { courseId, status }],\n queryFn: async () => {\n const data = await buiQuery<{ buiListCohorts: { items: BuiCohort[] } }>(\n `query BuiListCohorts($courseId: ID, $status: BuiCohortStatus) {\n buiListCohorts(courseId: $courseId, status: $status) { items { ${COHORT_FIELDS} } }\n }`,\n { courseId, status },\n baseUrl\n );\n return data.buiListCohorts.items;\n },\n });\n};\n\nexport const useBuiCreateCohort = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: BuiCreateCohortInput) => {\n const data = await buiQuery<{ buiCreateCohort: BuiCohort }>(\n `mutation BuiCreateCohort($input: BuiCreateCohortInput!) {\n buiCreateCohort(input: $input) { ${COHORT_FIELDS} }\n }`,\n { input },\n baseUrl\n );\n return data.buiCreateCohort;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.cohorts }),\n });\n};\n\n// ============================================================================\n// Skills\n// ============================================================================\n\nexport const useBuiSkills = (category?: BuiSkillCategory) => {\n const baseUrl = useGateway();\n return useQuery({\n queryKey: [...KEY.skills, { category }],\n queryFn: async () => {\n const data = await buiQuery<{ buiListSkills: { items: BuiSkill[] } }>(\n `query BuiListSkills($category: BuiSkillCategory) {\n buiListSkills(category: $category) { items { ${SKILL_FIELDS} } }\n }`,\n { category },\n baseUrl\n );\n return data.buiListSkills.items;\n },\n });\n};\n\nexport const useBuiCreateSkill = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: BuiCreateSkillInput) => {\n const data = await buiQuery<{ buiCreateSkill: BuiSkill }>(\n `mutation BuiCreateSkill($input: BuiCreateSkillInput!) {\n buiCreateSkill(input: $input) { ${SKILL_FIELDS} }\n }`,\n { input },\n baseUrl\n );\n return data.buiCreateSkill;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.skills }),\n });\n};\n\nexport const useBuiSkillStandards = (\n source?: BuiSkillStandard['source'],\n category?: BuiSkillCategory,\n enabled = true\n) => {\n const baseUrl = useGateway();\n return useQuery({\n queryKey: ['buildmyiq', 'skillStandards', { source, category }],\n enabled,\n queryFn: async () => {\n const data = await buiQuery<{ buiListSkillStandards: { items: BuiSkillStandard[] } }>(\n `query BuiListSkillStandards($source: BuiSkillStandardSource, $category: BuiSkillCategory) {\n buiListSkillStandards(source: $source, category: $category) { items { ${SKILL_STANDARD_FIELDS} } }\n }`,\n { source, category },\n baseUrl\n );\n return data.buiListSkillStandards.items;\n },\n });\n};\n\nexport const useBuiCertifications = (body?: BuiCertification['body'], enabled = true) => {\n const baseUrl = useGateway();\n return useQuery({\n queryKey: ['buildmyiq', 'certifications', { body }],\n enabled,\n queryFn: async () => {\n const data = await buiQuery<{ buiListCertifications: { items: BuiCertification[] } }>(\n `query BuiListCertifications($body: BuiCertificationBody) {\n buiListCertifications(body: $body) { items { ${CERTIFICATION_FIELDS} } }\n }`,\n { body },\n baseUrl\n );\n return data.buiListCertifications.items;\n },\n });\n};\n\nexport const useBuiSuggestSkillMappings = (name: string, description?: string, enabled = true) => {\n const baseUrl = useGateway();\n return useQuery({\n queryKey: ['buildmyiq', 'skillMappingSuggestions', { name, description }],\n enabled: enabled && name.trim().length > 0,\n queryFn: async () => {\n const data = await buiQuery<{ buiSuggestSkillMappings: BuiSkillMappingSuggestion[] }>(\n `query BuiSuggestSkillMappings($name: String!, $description: String, $limit: Int) {\n buiSuggestSkillMappings(name: $name, description: $description, limit: $limit) {\n skillStandard { ${SKILL_STANDARD_FIELDS} }\n confidence\n reason\n }\n }`,\n { name, description, limit: 5 },\n baseUrl\n );\n return data.buiSuggestSkillMappings;\n },\n });\n};\n\n// ============================================================================\n// Certificates\n// ============================================================================\n\n/**\n * Workspace-wide certificate list (instructor/admin). The backend dropped the\n * `learnerId` arg in the FERPA/IDOR hardening — a learner's OWN certificates\n * come from the self-scoped `useBuiMyCertificates` instead.\n */\nexport const useBuiCertificates = (courseId?: string, enabled = true) => {\n const baseUrl = useGateway();\n return useQuery({\n queryKey: [...KEY.certificates, { courseId }],\n enabled,\n queryFn: async () => {\n const data = await buiQuery<{ buiListCertificates: { items: BuiCertificate[] } }>(\n `query BuiListCertificates($courseId: ID) {\n buiListCertificates(courseId: $courseId) { items { ${CERTIFICATE_FIELDS} } }\n }`,\n { courseId },\n baseUrl\n );\n return data.buiListCertificates.items;\n },\n });\n};\n\n/** The signed-in learner's own certificates (self-scoped, no learnerId arg). */\nexport const useBuiMyCertificates = (courseId?: string, enabled = true) => {\n const baseUrl = useGateway();\n return useQuery({\n queryKey: [...KEY.certificates, 'mine', { courseId }],\n enabled,\n queryFn: async () => {\n const data = await buiQuery<{ buiMyCertificates: { items: BuiCertificate[] } }>(\n `query BuiMyCertificates($courseId: ID) {\n buiMyCertificates(courseId: $courseId) { items { ${CERTIFICATE_FIELDS} } }\n }`,\n { courseId },\n baseUrl\n );\n return data.buiMyCertificates.items;\n },\n });\n};\n\nexport const useBuiIssueCertificate = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: BuiIssueCertificateInput) => {\n const data = await buiQuery<{ buiIssueCertificate: BuiCertificate }>(\n `mutation BuiIssueCertificate($input: BuiIssueCertificateInput!) {\n buiIssueCertificate(input: $input) { ${CERTIFICATE_FIELDS} }\n }`,\n { input },\n baseUrl\n );\n return data.buiIssueCertificate;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.certificates }),\n });\n};\n\nexport const useBuiRevokeCertificate = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (id: string) => {\n const data = await buiQuery<{ buiRevokeCertificate: BuiCertificate }>(\n `mutation BuiRevokeCertificate($id: ID!) {\n buiRevokeCertificate(id: $id) { ${CERTIFICATE_FIELDS} }\n }`,\n { id },\n baseUrl\n );\n return data.buiRevokeCertificate;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.certificates }),\n });\n};\n\n// ============================================================================\n// AI Tutor (BOFF-4426) — grounded lesson Q&A chat\n// ============================================================================\n\n/**\n * The calling learner's own tutor sessions for a lesson (Type-C self-scoped\n * query — no `learnerId` arg on the wire; identity comes from the\n * authenticated actor server-side). Sessions come back newest-first with\n * their full message history, so the panel can restore an in-progress\n * conversation on mount.\n */\nexport const useBuiMyTutorSessions = (lessonId: string | undefined) => {\n const baseUrl = useGateway();\n return useQuery({\n queryKey: lessonId ? KEY.tutorSessions(lessonId) : ['buildmyiq', 'tutorSessions', 'nil'],\n enabled: Boolean(lessonId),\n queryFn: async () => {\n const data = await buiQuery<{ buiMyTutorSessions: { items: BuiTutorSession[] } }>(\n `query BuiMyTutorSessions($lessonId: ID) {\n buiMyTutorSessions(lessonId: $lessonId) { items { ${TUTOR_SESSION_FIELDS} } }\n }`,\n { lessonId },\n baseUrl\n );\n return data.buiMyTutorSessions.items;\n },\n });\n};\n\n/**\n * Ask the AI tutor a question about a lesson. Omit `sessionId` to start a\n * new session for that lesson; pass the current session's id to continue\n * the same conversation (multi-turn). Invalidates the lesson's tutor-session\n * query so the panel picks up the new USER + ASSISTANT turns.\n */\nexport const useBuiTutorAsk = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: BuiTutorAskInput) => {\n const data = await buiQuery<{ buiTutorAsk: BuiTutorAskPayload }>(\n `mutation BuiTutorAsk($input: BuiTutorAskInput!) {\n buiTutorAsk(input: $input) {\n session { ${TUTOR_SESSION_FIELDS} }\n answer { ${TUTOR_MESSAGE_FIELDS} }\n }\n }`,\n { input },\n baseUrl\n );\n return data.buiTutorAsk;\n },\n onSuccess: (payload) => {\n qc.invalidateQueries({ queryKey: KEY.tutorSessions(payload.session.lessonId) });\n },\n });\n};\n\n// ============================================================================\n// AI grading assist + admin copilot + AI course builder (BOFF-3724/4429/4434)\n// ============================================================================\n\nconst GRADE_SUGGESTION_FIELDS = `\n id submissionId proposedScore\n perQuestionFeedback {\n questionId feedback suggestedPoints\n criterionScores { criterionId title maxPoints suggestedPoints comment }\n }\n rubricRefs confidence inputRefs outcome createdAt\n`;\n\n/**\n * Proposes an AI-assisted score for a submission's open-ended questions —\n * NEVER writes the submission itself. The caller reviews/edits the result\n * and still commits via `useBuiGradeSubmission`. Throws (honest fail-clear)\n * when the backend has no LLM provider configured or the assessment has no\n * open-ended questions.\n */\nexport const useBuiSuggestGrade = () => {\n const baseUrl = useGateway();\n return useMutation({\n mutationFn: async (submissionId: string) => {\n const data = await buiQuery<{ buiSuggestGrade: BuiGradeSuggestion }>(\n `mutation BuiSuggestGrade($submissionId: ID!) {\n buiSuggestGrade(submissionId: $submissionId) { ${GRADE_SUGGESTION_FIELDS} }\n }`,\n { submissionId },\n baseUrl\n );\n return data.buiSuggestGrade;\n },\n });\n};\n\n/** Records whether the educator accepted or rejected a grading suggestion (measurable acceptance rate). */\nexport const useBuiReviewGradeSuggestion = () => {\n const baseUrl = useGateway();\n return useMutation({\n mutationFn: async (args: { id: string; outcome: BuiAiSuggestionOutcome }) => {\n const data = await buiQuery<{ buiReviewGradeSuggestion: BuiGradeSuggestion }>(\n `mutation BuiReviewGradeSuggestion($id: ID!, $outcome: BuiAiSuggestionOutcome!) {\n buiReviewGradeSuggestion(id: $id, outcome: $outcome) { ${GRADE_SUGGESTION_FIELDS} }\n }`,\n args,\n baseUrl\n );\n return data.buiReviewGradeSuggestion;\n },\n });\n};\n\n/**\n * AI-drafted course outline from a topic — an EPHEMERAL suggestion (not\n * persisted server-side); the instructor reviews/edits it before\n * `useBuiApplyCourseOutline` creates anything real.\n */\nexport const useBuiSuggestCourseOutline = () => {\n const baseUrl = useGateway();\n return useMutation({\n mutationFn: async (args: {\n topic: string;\n audienceLevel?: string | null;\n moduleCount?: number | null;\n }) => {\n const data = await buiQuery<{ buiSuggestCourseOutline: BuiCourseOutlineSuggestion }>(\n `mutation BuiSuggestCourseOutline($topic: String!, $audienceLevel: String, $moduleCount: Int) {\n buiSuggestCourseOutline(topic: $topic, audienceLevel: $audienceLevel, moduleCount: $moduleCount) {\n rationale confidence\n modules { title description lessons { title description } }\n }\n }`,\n args,\n baseUrl\n );\n return data.buiSuggestCourseOutline;\n },\n });\n};\n\n/** Creates a real DRAFT course + modules + lessons from a (possibly-edited) AI outline. */\nexport const useBuiApplyCourseOutline = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: BuiApplyCourseOutlineInput) => {\n const data = await buiQuery<{ buiApplyCourseOutline: BuiCourse }>(\n `mutation BuiApplyCourseOutline($input: BuiApplyCourseOutlineInput!) {\n buiApplyCourseOutline(input: $input) { ${COURSE_CORE_FIELDS} }\n }`,\n { input },\n baseUrl\n );\n return data.buiApplyCourseOutline;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.courses }),\n });\n};\n\n/**\n * Read-only admin Q&A grounded in real workspace stats — NEVER executes an\n * admin action. Stateless per call; the caller resends prior turns as\n * `history` for multi-turn context.\n */\nexport const useBuiAdminCopilotAsk = () => {\n const baseUrl = useGateway();\n return useMutation({\n mutationFn: async (args: { question: string; history: BuiAdminCopilotTurnInput[] }) => {\n const data = await buiQuery<{ buiAdminCopilotAsk: BuiAdminCopilotAnswer }>(\n `mutation BuiAdminCopilotAsk($question: String!, $history: [BuiAdminCopilotTurnInput!]) {\n buiAdminCopilotAsk(question: $question, history: $history) {\n answer contextCourseIds generatedAt\n }\n }`,\n args,\n baseUrl\n );\n return data.buiAdminCopilotAsk;\n },\n });\n};\n\n// ============================================================================\n// LTI 1.3 Advantage — platform registry (BOFF-4419)\n// ============================================================================\n\nconst LTI_PLATFORM_FIELDS = `\n id workspaceId name issuer clientId deploymentIds\n authLoginUrl authTokenUrl keySetUrl active createdAt updatedAt\n`;\n\nexport const useBuiLtiPlatforms = () => {\n const baseUrl = useGateway();\n return useQuery({\n queryKey: KEY.ltiPlatforms,\n queryFn: async () => {\n const data = await buiQuery<{ buiLtiPlatforms: BuiLtiPlatform[] }>(\n `query BuiLtiPlatforms { buiLtiPlatforms { ${LTI_PLATFORM_FIELDS} } }`,\n {},\n baseUrl\n );\n return data.buiLtiPlatforms;\n },\n });\n};\n\nexport const useBuiRegisterLtiPlatform = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (input: BuiRegisterLtiPlatformInput) => {\n const data = await buiQuery<{ buiRegisterLtiPlatform: BuiLtiPlatform }>(\n `mutation BuiRegisterLtiPlatform($input: BuiRegisterLtiPlatformInput!) {\n buiRegisterLtiPlatform(input: $input) { ${LTI_PLATFORM_FIELDS} }\n }`,\n { input },\n baseUrl\n );\n return data.buiRegisterLtiPlatform;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.ltiPlatforms }),\n });\n};\n\nexport const useBuiDeactivateLtiPlatform = () => {\n const baseUrl = useGateway();\n const qc = useQueryClient();\n return useMutation({\n mutationFn: async (id: string) => {\n const data = await buiQuery<{ buiDeactivateLtiPlatform: boolean }>(\n `mutation BuiDeactivateLtiPlatform($id: ID!) { buiDeactivateLtiPlatform(id: $id) }`,\n { id },\n baseUrl\n );\n return data.buiDeactivateLtiPlatform;\n },\n onSuccess: () => qc.invalidateQueries({ queryKey: KEY.ltiPlatforms }),\n });\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"DashboardPage.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/pages/DashboardPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAyB,MAAM,OAAO,CAAC;AA0DvD,eAAO,MAAM,aAAa,EAAE,EAgW3B,CAAC"}
1
+ {"version":3,"file":"DashboardPage.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/pages/DashboardPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAyB,MAAM,OAAO,CAAC;AA0DvD,eAAO,MAAM,aAAa,EAAE,EAkW3B,CAAC"}
@@ -10,7 +10,7 @@ import { LoadingView as c, Section as l } from "../components/StateViews.js";
10
10
  import { MiniBar as u } from "../components/MiniBar.js";
11
11
  import { Avatar as d } from "../components/Avatar.js";
12
12
  import { Sparkline as f } from "../components/Viz.js";
13
- import { useBuiCertificates as p, useBuiCourses as m, useBuiEnrollments as h } from "../hooks/useBuildmyiqApi.js";
13
+ import { useBuiCourses as p, useBuiMyCertificates as m, useBuiMyEnrollments as h } from "../hooks/useBuildmyiqApi.js";
14
14
  import "../hooks/index.js";
15
15
  import "../components/index.js";
16
16
  import { CourseCard as g } from "./_shared/CourseCard.js";
@@ -35,14 +35,13 @@ var k = (e, t = 8) => {
35
35
  "aria-hidden": "true",
36
36
  children: t
37
37
  }), j = () => {
38
- let { currentUser: n, navigateTo: j, workspaceId: M } = e(), N = t(), P = O(), F = n?.id, I = m({
38
+ let { currentUser: n, navigateTo: j, workspaceId: M } = e(), N = t(), P = O(), F = n?.id, I = p({
39
39
  status: "PUBLISHED",
40
40
  limit: 50
41
41
  }), L = h({
42
- learnerId: F,
43
42
  limit: 50,
44
43
  enabled: !!F
45
- }), R = p(void 0, F), z = I.data?.items ?? [], B = L.data?.items ?? [], V = R.data ?? [], H = _(() => B.filter((e) => e.status === "ENROLLED" || e.status === "IN_PROGRESS").sort((e, t) => (t.lastActiveAt ?? t.enrolledAt).localeCompare(e.lastActiveAt ?? e.enrolledAt)), [B]), U = B.filter((e) => e.status === "COMPLETED").length, W = n?.firstName || n?.name?.split(" ")[0] || "there", G = [n?.firstName, n?.lastName].filter(Boolean).join(" ").trim(), K = n?.name || G || (W === "there" ? "" : W), q = n?.role ? a(n.role) : n?.email, J = I.isLoading || !!F && L.isLoading, Y = _(() => {
44
+ }), R = m(void 0, !!F), z = I.data?.items ?? [], B = L.data?.items ?? [], V = R.data ?? [], H = _(() => B.filter((e) => e.status === "ENROLLED" || e.status === "IN_PROGRESS").sort((e, t) => (t.lastActiveAt ?? t.enrolledAt).localeCompare(e.lastActiveAt ?? e.enrolledAt)), [B]), U = B.filter((e) => e.status === "COMPLETED").length, W = n?.firstName || n?.name?.split(" ")[0] || "there", G = [n?.firstName, n?.lastName].filter(Boolean).join(" ").trim(), K = n?.name || G || (W === "there" ? "" : W), q = n?.role ? a(n.role) : n?.email, J = I.isLoading || !!F && L.isLoading, Y = _(() => {
46
45
  let e = /* @__PURE__ */ new Map();
47
46
  for (let t of z) e.set(t.id, t);
48
47
  return e;
@@ -1 +1 @@
1
- {"version":3,"file":"DashboardPage.js","names":[],"sources":["../../../src/buildmyiq/pages/DashboardPage.tsx"],"sourcesContent":["/**\n * DashboardPage — the Buildmyiq landing surface.\n * @module buildmyiq/pages\n *\n * A learner + educator overview built entirely from real `bui*` data:\n * - KPI row: published courses, my active enrollments, completed, certificates\n * — each stat card carries a tinted icon tile and (where derivable) a\n * Sparkline trend built from the REAL timestamps of the underlying rows.\n * - Continue learning: the caller's in-progress enrollments as rich cards with\n * real cover art, summary, a progress bar and a Continue action.\n * - Explore: recently published catalog courses.\n * Every number, row, avatar and trend is derived from live hooks; empty states\n * are honest and no value is fabricated.\n */\n\nimport type { FC, ReactNode, MouseEvent } from 'react';\nimport { useMemo } from 'react';\nimport { BookOpen, Clock, CheckCircle2, Award, ArrowRight, Play } from 'lucide-react';\nimport { Button, GlassCard } from '@burdenoff/fe-libs/ui';\nimport { useEventBus } from '@burdenoff/fe-libs/shared/events';\nimport {\n PageLayout,\n MetricsCard,\n EmptyState,\n LoadingView,\n Section,\n MiniBar,\n Avatar,\n Sparkline,\n} from '../components';\nimport { CourseCard } from './_shared';\nimport { cn } from '../utils/cn';\nimport { useBuildmyiq } from '../context';\nimport { useBuiCourses, useBuiEnrollments, useBuiCertificates } from '../hooks';\nimport { useTr } from '../shared/hooks/useTr';\nimport { formatRelativeTime, formatEnumLabel } from '../utils/formatters';\nimport type { BuiCourse, BuiEnrollment } from '../types';\n\n/**\n * Build a cumulative-count trend series from a set of REAL event timestamps\n * (e.g. `publishedAt`, `enrolledAt`, `issuedAt`). The span from the earliest\n * event to now is split into `buckets` slots and each slot holds the running\n * count of events up to its boundary — a genuine, non-fabricated growth line.\n * Returns fewer than two points when there is not enough history to plot; the\n * caller then omits the sparkline rather than drawing a meaningless flat line.\n */\nconst cumulativeSeries = (isoDates: Array<string | null | undefined>, buckets = 8): number[] => {\n const times = isoDates\n .map((d) => (d ? new Date(d).getTime() : Number.NaN))\n .filter((t) => !Number.isNaN(t))\n .sort((a, b) => a - b);\n if (times.length < 2) return times.length === 1 ? [1] : [];\n const start = times[0];\n const end = Date.now();\n const span = Math.max(1, end - start);\n const series: number[] = [];\n for (let i = 1; i <= buckets; i += 1) {\n const boundary = start + (span * i) / buckets;\n series.push(times.filter((t) => t <= boundary).length);\n }\n return series;\n};\n\n/** A tinted, rounded icon tile used in the KPI stat cards. */\nconst StatIcon: FC<{ tint: string; children: ReactNode }> = ({ tint, children }) => (\n <span\n className={cn('inline-flex h-10 w-10 items-center justify-center rounded-xl', tint)}\n aria-hidden=\"true\"\n >\n {children}\n </span>\n);\n\nexport const DashboardPage: FC = () => {\n const { currentUser, navigateTo, workspaceId } = useBuildmyiq();\n const tr = useTr();\n const bus = useEventBus();\n const learnerId = currentUser?.id;\n\n const coursesQuery = useBuiCourses({ status: 'PUBLISHED', limit: 50 });\n const enrollmentsQuery = useBuiEnrollments({ learnerId, limit: 50, enabled: Boolean(learnerId) });\n const certsQuery = useBuiCertificates(undefined, learnerId);\n\n const courses: BuiCourse[] = coursesQuery.data?.items ?? [];\n const enrollments: BuiEnrollment[] = enrollmentsQuery.data?.items ?? [];\n const certificates = certsQuery.data ?? [];\n\n const activeEnrollments = useMemo(\n () =>\n enrollments\n .filter((e) => e.status === 'ENROLLED' || e.status === 'IN_PROGRESS')\n .sort((a, b) =>\n (b.lastActiveAt ?? b.enrolledAt).localeCompare(a.lastActiveAt ?? a.enrolledAt)\n ),\n [enrollments]\n );\n const completedCount = enrollments.filter((e) => e.status === 'COMPLETED').length;\n\n const firstName = currentUser?.firstName || currentUser?.name?.split(' ')[0] || 'there';\n const fullName = [currentUser?.firstName, currentUser?.lastName].filter(Boolean).join(' ').trim();\n const displayName = currentUser?.name || fullName || (firstName === 'there' ? '' : firstName);\n const roleLabel = currentUser?.role ? formatEnumLabel(currentUser.role) : currentUser?.email;\n\n const isLoading = coursesQuery.isLoading || (Boolean(learnerId) && enrollmentsQuery.isLoading);\n\n const courseById = useMemo(() => {\n const map = new Map<string, BuiCourse>();\n for (const c of courses) map.set(c.id, c);\n return map;\n }, [courses]);\n\n // REAL trend lines — cumulative growth from each entity's own timestamps.\n const trends = useMemo(\n () => ({\n published: cumulativeSeries(courses.map((c) => c.publishedAt ?? c.createdAt)),\n inProgress: cumulativeSeries(enrollments.map((e) => e.enrolledAt)),\n completed: cumulativeSeries(\n enrollments.filter((e) => e.status === 'COMPLETED').map((e) => e.completedAt ?? e.updatedAt)\n ),\n certificates: cumulativeSeries(certificates.map((c) => c.issuedAt)),\n }),\n [courses, enrollments, certificates]\n );\n\n const kpis: Array<{\n key: string;\n icon: ReactNode;\n tint: string;\n value: string | number;\n label: string;\n featured?: boolean;\n series: number[];\n sparkColor: string;\n }> = [\n {\n key: 'published',\n icon: <BookOpen className=\"h-5 w-5\" />,\n tint: 'bg-[var(--color-accent-soft)] text-[var(--color-accent-primary)]',\n value: `${courses.length}${coursesQuery.data?.pageInfo?.hasMore ? '+' : ''}`,\n label: tr('buildmyiq.dashboard.publishedCourses', 'Published courses'),\n featured: true,\n series: trends.published,\n sparkColor: 'var(--color-accent-primary)',\n },\n {\n key: 'in-progress',\n icon: <Clock className=\"h-5 w-5\" />,\n tint: 'bg-[color-mix(in_srgb,var(--color-accent-teal)_16%,transparent)] text-[var(--color-accent-teal)]',\n value: activeEnrollments.length,\n label: tr('buildmyiq.dashboard.coursesInProgress', 'Courses in progress'),\n series: trends.inProgress,\n sparkColor: 'var(--color-accent-teal)',\n },\n {\n key: 'completed',\n icon: <CheckCircle2 className=\"h-5 w-5\" />,\n tint: 'bg-status-success-bg-subtle text-status-success-text',\n value: completedCount,\n label: tr('buildmyiq.dashboard.completed', 'Completed'),\n series: trends.completed,\n sparkColor: 'var(--color-status-success-text)',\n },\n {\n key: 'certificates',\n icon: <Award className=\"h-5 w-5\" />,\n tint: 'bg-status-warning-bg-subtle text-status-warning-text',\n value: certificates.length,\n label: tr('buildmyiq.dashboard.certificatesEarned', 'Certificates earned'),\n series: trends.certificates,\n sparkColor: 'var(--color-status-warning-text)',\n },\n ];\n\n return (\n <PageLayout\n title={tr('buildmyiq.dashboard.welcomeBack', 'Welcome back, {{name}}', { name: firstName })}\n description={tr(\n 'buildmyiq.dashboard.subtitle',\n 'Your learning, teaching and credentialing — all in one workspace.'\n )}\n titleGradient\n aurora\n actions={\n currentUser ? (\n <div className=\"flex w-fit items-center gap-2.5 rounded-full border border-border-seam bg-bg-surface/60 py-1.5 pl-1.5 pr-3.5 backdrop-blur\">\n <Avatar\n name={displayName || null}\n seed={currentUser.id}\n src={currentUser.avatar}\n size=\"sm\"\n />\n {(displayName || roleLabel) && (\n <div className=\"min-w-0 leading-tight\">\n {displayName && (\n <p className=\"truncate text-sm font-medium text-text-primary\">{displayName}</p>\n )}\n {roleLabel && (\n <p className=\"max-w-[10rem] truncate text-xs text-text-secondary\">{roleLabel}</p>\n )}\n </div>\n )}\n </div>\n ) : undefined\n }\n >\n {isLoading ? (\n <LoadingView\n label={tr('buildmyiq.dashboard.loading', 'Loading your learning dashboard…')}\n />\n ) : (\n <div className=\"flex flex-col gap-8\" data-tour=\"buildmyiq-dashboard-content\">\n {/* KPI row */}\n <div\n className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4\"\n data-tour=\"buildmyiq-dashboard-kpis\"\n >\n {kpis.map((k) => (\n <MetricsCard\n key={k.key}\n icon={<StatIcon tint={k.tint}>{k.icon}</StatIcon>}\n value={k.value}\n label={k.label}\n featured={k.featured}\n pill={\n k.series.length >= 2 ? (\n <Sparkline data={k.series} color={k.sparkColor} />\n ) : undefined\n }\n />\n ))}\n </div>\n\n {/* Continue learning */}\n <Section\n title={tr('buildmyiq.dashboard.continueLearning', 'Continue learning')}\n description={tr(\n 'buildmyiq.dashboard.continueLearningDescription',\n 'Pick up where you left off.'\n )}\n action={\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => {\n bus.emitCustom('buildmyiq.dashboard.my_learning_clicked', { workspaceId });\n navigateTo('learning');\n }}\n data-tour=\"buildmyiq-dashboard-my-learning\"\n >\n {tr('buildmyiq.dashboard.myLearning', 'My learning')}{' '}\n <ArrowRight className=\"ml-1 h-4 w-4\" />\n </Button>\n }\n >\n {activeEnrollments.length === 0 ? (\n <EmptyState\n illustration=\"onboarding\"\n title={tr('buildmyiq.dashboard.noActiveCoursesTitle', 'No active courses yet')}\n description={tr(\n 'buildmyiq.dashboard.noActiveCoursesDescription',\n 'Browse the catalog and enrol in a course to start learning.'\n )}\n action={\n <Button\n onClick={() => {\n bus.emitCustom('buildmyiq.dashboard.explore_catalog_clicked', {\n workspaceId,\n });\n navigateTo('catalog');\n }}\n data-tour=\"buildmyiq-dashboard-explore-catalog\"\n >\n {tr('common.explore', 'Explore the catalog')}\n </Button>\n }\n />\n ) : (\n <div\n className=\"grid grid-cols-1 gap-4 md:grid-cols-2\"\n data-tour=\"buildmyiq-dashboard-continue-cards\"\n >\n {activeEnrollments.slice(0, 4).map((e) => {\n const course = courseById.get(e.courseId);\n const title = course?.title ?? 'Course';\n const goToCourse = (ev?: MouseEvent) => {\n ev?.stopPropagation();\n navigateTo(`learning/${e.id}`);\n };\n return (\n <GlassCard\n key={e.id}\n treatment=\"plain\"\n className=\"group flex cursor-pointer flex-col overflow-hidden transition-shadow hover:shadow-lg sm:flex-row\"\n onClick={() => goToCourse()}\n >\n {/* Cover / play thumbnail */}\n <div className=\"relative h-36 w-full shrink-0 overflow-hidden bg-bg-sunken sm:h-auto sm:w-40\">\n {course?.cover?.downloadUrl ? (\n <img\n src={course.cover.downloadUrl}\n alt=\"\"\n className=\"h-full w-full object-cover transition-transform duration-500 group-hover:scale-105\"\n />\n ) : (\n <div className=\"flex h-full w-full items-center justify-center bg-gradient-to-br from-[var(--color-accent-soft)] to-bg-sunken\">\n <BookOpen className=\"h-8 w-8 text-text-secondary opacity-60\" />\n </div>\n )}\n <div className=\"absolute inset-0 flex items-center justify-center\">\n <span className=\"inline-flex h-11 w-11 items-center justify-center rounded-full bg-bg-surface/85 shadow-lg backdrop-blur transition-transform duration-200 group-hover:scale-110\">\n <Play className=\"ml-0.5 h-5 w-5 fill-current text-[var(--color-accent-primary)]\" />\n </span>\n </div>\n </div>\n\n {/* Content */}\n <div className=\"flex min-w-0 flex-1 flex-col gap-2 p-4\">\n <p className=\"line-clamp-1 font-semibold text-text-primary\">{title}</p>\n {course?.summary && (\n <p className=\"line-clamp-2 text-sm text-text-secondary\">\n {course.summary}\n </p>\n )}\n <div className=\"mt-auto space-y-2.5 pt-2\">\n <div className=\"flex items-center gap-2\">\n <MiniBar value={e.progressPct} className=\"flex-1\" />\n <span className=\"shrink-0 text-xs font-semibold tabular-nums text-text-secondary\">\n {e.progressPct}%\n </span>\n </div>\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"truncate text-xs text-text-secondary\">\n Active {formatRelativeTime(e.lastActiveAt ?? e.enrolledAt)}\n </span>\n <Button\n size=\"sm\"\n className=\"min-h-[40px] shrink-0\"\n onClick={(ev: MouseEvent) => {\n bus.emitCustom('buildmyiq.dashboard.continue_course_clicked', {\n workspaceId,\n enrollmentId: e.id,\n courseId: e.courseId,\n });\n goToCourse(ev);\n }}\n data-tour=\"buildmyiq-dashboard-continue-button\"\n >\n <Play className=\"mr-1 h-4 w-4 fill-current\" />{' '}\n {tr('common.continue', 'Continue')}\n </Button>\n </div>\n </div>\n </div>\n </GlassCard>\n );\n })}\n </div>\n )}\n </Section>\n\n {/* Explore catalog */}\n <Section\n title={tr('buildmyiq.dashboard.exploreCatalog', 'Explore the catalog')}\n description={tr(\n 'buildmyiq.dashboard.exploreCatalogDescription',\n 'Newly published courses in your workspace.'\n )}\n action={\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => {\n bus.emitCustom('buildmyiq.dashboard.browse_all_catalog_clicked', { workspaceId });\n navigateTo('catalog');\n }}\n data-tour=\"buildmyiq-dashboard-browse-all\"\n >\n {tr('common.browseAll', 'Browse all')} <ArrowRight className=\"ml-1 h-4 w-4\" />\n </Button>\n }\n >\n {courses.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title={tr(\n 'buildmyiq.dashboard.noPublishedCoursesTitle',\n 'No published courses yet'\n )}\n description={tr(\n 'buildmyiq.dashboard.noPublishedCoursesDescription',\n 'Create and publish a course in Authoring to populate the catalog.'\n )}\n action={\n <Button\n onClick={() => {\n bus.emitCustom('buildmyiq.dashboard.go_to_authoring_clicked', {\n workspaceId,\n });\n navigateTo('authoring');\n }}\n data-tour=\"buildmyiq-dashboard-go-authoring\"\n >\n {tr('common.create', 'Go to Authoring')}\n </Button>\n }\n />\n ) : (\n <div\n className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\"\n data-tour=\"buildmyiq-dashboard-catalog-grid\"\n >\n {courses.slice(0, 8).map((course) => (\n <CourseCard\n key={course.id}\n course={course}\n onClick={() => navigateTo(`catalog/${course.id}`)}\n />\n ))}\n </div>\n )}\n </Section>\n </div>\n )}\n </PageLayout>\n );\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"DashboardPage.js","names":[],"sources":["../../../src/buildmyiq/pages/DashboardPage.tsx"],"sourcesContent":["/**\n * DashboardPage — the Buildmyiq landing surface.\n * @module buildmyiq/pages\n *\n * A learner + educator overview built entirely from real `bui*` data:\n * - KPI row: published courses, my active enrollments, completed, certificates\n * — each stat card carries a tinted icon tile and (where derivable) a\n * Sparkline trend built from the REAL timestamps of the underlying rows.\n * - Continue learning: the caller's in-progress enrollments as rich cards with\n * real cover art, summary, a progress bar and a Continue action.\n * - Explore: recently published catalog courses.\n * Every number, row, avatar and trend is derived from live hooks; empty states\n * are honest and no value is fabricated.\n */\n\nimport type { FC, ReactNode, MouseEvent } from 'react';\nimport { useMemo } from 'react';\nimport { BookOpen, Clock, CheckCircle2, Award, ArrowRight, Play } from 'lucide-react';\nimport { Button, GlassCard } from '@burdenoff/fe-libs/ui';\nimport { useEventBus } from '@burdenoff/fe-libs/shared/events';\nimport {\n PageLayout,\n MetricsCard,\n EmptyState,\n LoadingView,\n Section,\n MiniBar,\n Avatar,\n Sparkline,\n} from '../components';\nimport { CourseCard } from './_shared';\nimport { cn } from '../utils/cn';\nimport { useBuildmyiq } from '../context';\nimport { useBuiCourses, useBuiMyEnrollments, useBuiMyCertificates } from '../hooks';\nimport { useTr } from '../shared/hooks/useTr';\nimport { formatRelativeTime, formatEnumLabel } from '../utils/formatters';\nimport type { BuiCourse, BuiEnrollment } from '../types';\n\n/**\n * Build a cumulative-count trend series from a set of REAL event timestamps\n * (e.g. `publishedAt`, `enrolledAt`, `issuedAt`). The span from the earliest\n * event to now is split into `buckets` slots and each slot holds the running\n * count of events up to its boundary — a genuine, non-fabricated growth line.\n * Returns fewer than two points when there is not enough history to plot; the\n * caller then omits the sparkline rather than drawing a meaningless flat line.\n */\nconst cumulativeSeries = (isoDates: Array<string | null | undefined>, buckets = 8): number[] => {\n const times = isoDates\n .map((d) => (d ? new Date(d).getTime() : Number.NaN))\n .filter((t) => !Number.isNaN(t))\n .sort((a, b) => a - b);\n if (times.length < 2) return times.length === 1 ? [1] : [];\n const start = times[0];\n const end = Date.now();\n const span = Math.max(1, end - start);\n const series: number[] = [];\n for (let i = 1; i <= buckets; i += 1) {\n const boundary = start + (span * i) / buckets;\n series.push(times.filter((t) => t <= boundary).length);\n }\n return series;\n};\n\n/** A tinted, rounded icon tile used in the KPI stat cards. */\nconst StatIcon: FC<{ tint: string; children: ReactNode }> = ({ tint, children }) => (\n <span\n className={cn('inline-flex h-10 w-10 items-center justify-center rounded-xl', tint)}\n aria-hidden=\"true\"\n >\n {children}\n </span>\n);\n\nexport const DashboardPage: FC = () => {\n const { currentUser, navigateTo, workspaceId } = useBuildmyiq();\n const tr = useTr();\n const bus = useEventBus();\n const learnerId = currentUser?.id;\n\n const coursesQuery = useBuiCourses({ status: 'PUBLISHED', limit: 50 });\n // Self-scoped reads of the signed-in learner's own records (FERPA/IDOR\n // hardening removed the learnerId arg from the workspace-wide lists).\n const enrollmentsQuery = useBuiMyEnrollments({ limit: 50, enabled: Boolean(learnerId) });\n const certsQuery = useBuiMyCertificates(undefined, Boolean(learnerId));\n\n const courses: BuiCourse[] = coursesQuery.data?.items ?? [];\n const enrollments: BuiEnrollment[] = enrollmentsQuery.data?.items ?? [];\n const certificates = certsQuery.data ?? [];\n\n const activeEnrollments = useMemo(\n () =>\n enrollments\n .filter((e) => e.status === 'ENROLLED' || e.status === 'IN_PROGRESS')\n .sort((a, b) =>\n (b.lastActiveAt ?? b.enrolledAt).localeCompare(a.lastActiveAt ?? a.enrolledAt)\n ),\n [enrollments]\n );\n const completedCount = enrollments.filter((e) => e.status === 'COMPLETED').length;\n\n const firstName = currentUser?.firstName || currentUser?.name?.split(' ')[0] || 'there';\n const fullName = [currentUser?.firstName, currentUser?.lastName].filter(Boolean).join(' ').trim();\n const displayName = currentUser?.name || fullName || (firstName === 'there' ? '' : firstName);\n const roleLabel = currentUser?.role ? formatEnumLabel(currentUser.role) : currentUser?.email;\n\n const isLoading = coursesQuery.isLoading || (Boolean(learnerId) && enrollmentsQuery.isLoading);\n\n const courseById = useMemo(() => {\n const map = new Map<string, BuiCourse>();\n for (const c of courses) map.set(c.id, c);\n return map;\n }, [courses]);\n\n // REAL trend lines — cumulative growth from each entity's own timestamps.\n const trends = useMemo(\n () => ({\n published: cumulativeSeries(courses.map((c) => c.publishedAt ?? c.createdAt)),\n inProgress: cumulativeSeries(enrollments.map((e) => e.enrolledAt)),\n completed: cumulativeSeries(\n enrollments.filter((e) => e.status === 'COMPLETED').map((e) => e.completedAt ?? e.updatedAt)\n ),\n certificates: cumulativeSeries(certificates.map((c) => c.issuedAt)),\n }),\n [courses, enrollments, certificates]\n );\n\n const kpis: Array<{\n key: string;\n icon: ReactNode;\n tint: string;\n value: string | number;\n label: string;\n featured?: boolean;\n series: number[];\n sparkColor: string;\n }> = [\n {\n key: 'published',\n icon: <BookOpen className=\"h-5 w-5\" />,\n tint: 'bg-[var(--color-accent-soft)] text-[var(--color-accent-primary)]',\n value: `${courses.length}${coursesQuery.data?.pageInfo?.hasMore ? '+' : ''}`,\n label: tr('buildmyiq.dashboard.publishedCourses', 'Published courses'),\n featured: true,\n series: trends.published,\n sparkColor: 'var(--color-accent-primary)',\n },\n {\n key: 'in-progress',\n icon: <Clock className=\"h-5 w-5\" />,\n tint: 'bg-[color-mix(in_srgb,var(--color-accent-teal)_16%,transparent)] text-[var(--color-accent-teal)]',\n value: activeEnrollments.length,\n label: tr('buildmyiq.dashboard.coursesInProgress', 'Courses in progress'),\n series: trends.inProgress,\n sparkColor: 'var(--color-accent-teal)',\n },\n {\n key: 'completed',\n icon: <CheckCircle2 className=\"h-5 w-5\" />,\n tint: 'bg-status-success-bg-subtle text-status-success-text',\n value: completedCount,\n label: tr('buildmyiq.dashboard.completed', 'Completed'),\n series: trends.completed,\n sparkColor: 'var(--color-status-success-text)',\n },\n {\n key: 'certificates',\n icon: <Award className=\"h-5 w-5\" />,\n tint: 'bg-status-warning-bg-subtle text-status-warning-text',\n value: certificates.length,\n label: tr('buildmyiq.dashboard.certificatesEarned', 'Certificates earned'),\n series: trends.certificates,\n sparkColor: 'var(--color-status-warning-text)',\n },\n ];\n\n return (\n <PageLayout\n title={tr('buildmyiq.dashboard.welcomeBack', 'Welcome back, {{name}}', { name: firstName })}\n description={tr(\n 'buildmyiq.dashboard.subtitle',\n 'Your learning, teaching and credentialing — all in one workspace.'\n )}\n titleGradient\n aurora\n actions={\n currentUser ? (\n <div className=\"flex w-fit items-center gap-2.5 rounded-full border border-border-seam bg-bg-surface/60 py-1.5 pl-1.5 pr-3.5 backdrop-blur\">\n <Avatar\n name={displayName || null}\n seed={currentUser.id}\n src={currentUser.avatar}\n size=\"sm\"\n />\n {(displayName || roleLabel) && (\n <div className=\"min-w-0 leading-tight\">\n {displayName && (\n <p className=\"truncate text-sm font-medium text-text-primary\">{displayName}</p>\n )}\n {roleLabel && (\n <p className=\"max-w-[10rem] truncate text-xs text-text-secondary\">{roleLabel}</p>\n )}\n </div>\n )}\n </div>\n ) : undefined\n }\n >\n {isLoading ? (\n <LoadingView\n label={tr('buildmyiq.dashboard.loading', 'Loading your learning dashboard…')}\n />\n ) : (\n <div className=\"flex flex-col gap-8\" data-tour=\"buildmyiq-dashboard-content\">\n {/* KPI row */}\n <div\n className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-4\"\n data-tour=\"buildmyiq-dashboard-kpis\"\n >\n {kpis.map((k) => (\n <MetricsCard\n key={k.key}\n icon={<StatIcon tint={k.tint}>{k.icon}</StatIcon>}\n value={k.value}\n label={k.label}\n featured={k.featured}\n pill={\n k.series.length >= 2 ? (\n <Sparkline data={k.series} color={k.sparkColor} />\n ) : undefined\n }\n />\n ))}\n </div>\n\n {/* Continue learning */}\n <Section\n title={tr('buildmyiq.dashboard.continueLearning', 'Continue learning')}\n description={tr(\n 'buildmyiq.dashboard.continueLearningDescription',\n 'Pick up where you left off.'\n )}\n action={\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => {\n bus.emitCustom('buildmyiq.dashboard.my_learning_clicked', { workspaceId });\n navigateTo('learning');\n }}\n data-tour=\"buildmyiq-dashboard-my-learning\"\n >\n {tr('buildmyiq.dashboard.myLearning', 'My learning')}{' '}\n <ArrowRight className=\"ml-1 h-4 w-4\" />\n </Button>\n }\n >\n {activeEnrollments.length === 0 ? (\n <EmptyState\n illustration=\"onboarding\"\n title={tr('buildmyiq.dashboard.noActiveCoursesTitle', 'No active courses yet')}\n description={tr(\n 'buildmyiq.dashboard.noActiveCoursesDescription',\n 'Browse the catalog and enrol in a course to start learning.'\n )}\n action={\n <Button\n onClick={() => {\n bus.emitCustom('buildmyiq.dashboard.explore_catalog_clicked', {\n workspaceId,\n });\n navigateTo('catalog');\n }}\n data-tour=\"buildmyiq-dashboard-explore-catalog\"\n >\n {tr('common.explore', 'Explore the catalog')}\n </Button>\n }\n />\n ) : (\n <div\n className=\"grid grid-cols-1 gap-4 md:grid-cols-2\"\n data-tour=\"buildmyiq-dashboard-continue-cards\"\n >\n {activeEnrollments.slice(0, 4).map((e) => {\n const course = courseById.get(e.courseId);\n const title = course?.title ?? 'Course';\n const goToCourse = (ev?: MouseEvent) => {\n ev?.stopPropagation();\n navigateTo(`learning/${e.id}`);\n };\n return (\n <GlassCard\n key={e.id}\n treatment=\"plain\"\n className=\"group flex cursor-pointer flex-col overflow-hidden transition-shadow hover:shadow-lg sm:flex-row\"\n onClick={() => goToCourse()}\n >\n {/* Cover / play thumbnail */}\n <div className=\"relative h-36 w-full shrink-0 overflow-hidden bg-bg-sunken sm:h-auto sm:w-40\">\n {course?.cover?.downloadUrl ? (\n <img\n src={course.cover.downloadUrl}\n alt=\"\"\n className=\"h-full w-full object-cover transition-transform duration-500 group-hover:scale-105\"\n />\n ) : (\n <div className=\"flex h-full w-full items-center justify-center bg-gradient-to-br from-[var(--color-accent-soft)] to-bg-sunken\">\n <BookOpen className=\"h-8 w-8 text-text-secondary opacity-60\" />\n </div>\n )}\n <div className=\"absolute inset-0 flex items-center justify-center\">\n <span className=\"inline-flex h-11 w-11 items-center justify-center rounded-full bg-bg-surface/85 shadow-lg backdrop-blur transition-transform duration-200 group-hover:scale-110\">\n <Play className=\"ml-0.5 h-5 w-5 fill-current text-[var(--color-accent-primary)]\" />\n </span>\n </div>\n </div>\n\n {/* Content */}\n <div className=\"flex min-w-0 flex-1 flex-col gap-2 p-4\">\n <p className=\"line-clamp-1 font-semibold text-text-primary\">{title}</p>\n {course?.summary && (\n <p className=\"line-clamp-2 text-sm text-text-secondary\">\n {course.summary}\n </p>\n )}\n <div className=\"mt-auto space-y-2.5 pt-2\">\n <div className=\"flex items-center gap-2\">\n <MiniBar value={e.progressPct} className=\"flex-1\" />\n <span className=\"shrink-0 text-xs font-semibold tabular-nums text-text-secondary\">\n {e.progressPct}%\n </span>\n </div>\n <div className=\"flex items-center justify-between gap-2\">\n <span className=\"truncate text-xs text-text-secondary\">\n Active {formatRelativeTime(e.lastActiveAt ?? e.enrolledAt)}\n </span>\n <Button\n size=\"sm\"\n className=\"min-h-[40px] shrink-0\"\n onClick={(ev: MouseEvent) => {\n bus.emitCustom('buildmyiq.dashboard.continue_course_clicked', {\n workspaceId,\n enrollmentId: e.id,\n courseId: e.courseId,\n });\n goToCourse(ev);\n }}\n data-tour=\"buildmyiq-dashboard-continue-button\"\n >\n <Play className=\"mr-1 h-4 w-4 fill-current\" />{' '}\n {tr('common.continue', 'Continue')}\n </Button>\n </div>\n </div>\n </div>\n </GlassCard>\n );\n })}\n </div>\n )}\n </Section>\n\n {/* Explore catalog */}\n <Section\n title={tr('buildmyiq.dashboard.exploreCatalog', 'Explore the catalog')}\n description={tr(\n 'buildmyiq.dashboard.exploreCatalogDescription',\n 'Newly published courses in your workspace.'\n )}\n action={\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={() => {\n bus.emitCustom('buildmyiq.dashboard.browse_all_catalog_clicked', { workspaceId });\n navigateTo('catalog');\n }}\n data-tour=\"buildmyiq-dashboard-browse-all\"\n >\n {tr('common.browseAll', 'Browse all')} <ArrowRight className=\"ml-1 h-4 w-4\" />\n </Button>\n }\n >\n {courses.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title={tr(\n 'buildmyiq.dashboard.noPublishedCoursesTitle',\n 'No published courses yet'\n )}\n description={tr(\n 'buildmyiq.dashboard.noPublishedCoursesDescription',\n 'Create and publish a course in Authoring to populate the catalog.'\n )}\n action={\n <Button\n onClick={() => {\n bus.emitCustom('buildmyiq.dashboard.go_to_authoring_clicked', {\n workspaceId,\n });\n navigateTo('authoring');\n }}\n data-tour=\"buildmyiq-dashboard-go-authoring\"\n >\n {tr('common.create', 'Go to Authoring')}\n </Button>\n }\n />\n ) : (\n <div\n className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\"\n data-tour=\"buildmyiq-dashboard-catalog-grid\"\n >\n {courses.slice(0, 8).map((course) => (\n <CourseCard\n key={course.id}\n course={course}\n onClick={() => navigateTo(`catalog/${course.id}`)}\n />\n ))}\n </div>\n )}\n </Section>\n </div>\n )}\n </PageLayout>\n );\n};\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"TakeAssessmentPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/assessments/TakeAssessmentPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAa,MAAM,OAAO,CAAC;AAkE3C,eAAO,MAAM,kBAAkB,EAAE,EA0WhC,CAAC"}
1
+ {"version":3,"file":"TakeAssessmentPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/assessments/TakeAssessmentPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAa,MAAM,OAAO,CAAC;AAkE3C,eAAO,MAAM,kBAAkB,EAAE,EAyWhC,CAAC"}
@@ -7,7 +7,7 @@ import { StatusBadge as a } from "../../components/StatusBadge.js";
7
7
  import { EmptyState as o } from "../../components/EmptyState.js";
8
8
  import { ErrorView as s, LoadingView as c } from "../../components/StateViews.js";
9
9
  import { ProgressRing as l } from "../../components/Viz.js";
10
- import { useBuiAssessment as u, useBuiEnrollments as d, useBuiSubmitAssessment as f } from "../../hooks/useBuildmyiqApi.js";
10
+ import { useBuiAssessment as u, useBuiMyEnrollments as d, useBuiSubmitAssessment as f } from "../../hooks/useBuildmyiqApi.js";
11
11
  import "../../hooks/index.js";
12
12
  import "../../components/index.js";
13
13
  import { useMemo as p, useState as m } from "react";
@@ -36,7 +36,6 @@ var j = [{ label: "True" }, { label: "False" }], M = ({ icon: e, label: t, value
36
36
  })]
37
37
  }), N = () => {
38
38
  let { assessmentId: N } = C(), { currentUser: P, navigateTo: F } = e(), I = u(N), L = I.data ?? null, R = d({
39
- learnerId: P?.id,
40
39
  courseId: L?.courseId,
41
40
  enabled: !!(P?.id && L?.courseId)
42
41
  }).data?.items?.[0]?.id, z = f(), [B, V] = m({}), [H, U] = m(null), W = p(() => [...L?.questions ?? []].sort((e, t) => e.position - t.position), [L]), G = (e, t) => {