bitsnap-react 1.16.0 → 1.18.0

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 (125) hide show
  1. package/AGENTS.md +5 -0
  2. package/README.md +87 -0
  3. package/dist/CartProvider-ECQQH3PS.css +2 -0
  4. package/dist/CartProvider-ECQQH3PS.css.map +1 -0
  5. package/dist/CartProvider-OEI526IN.js +2 -0
  6. package/dist/{CartProvider-VGZMDVFJ.js.map → CartProvider-OEI526IN.js.map} +1 -1
  7. package/dist/CartProvider-RYAA3JDA.mjs +2 -0
  8. package/dist/{PanelModelFiles-NYCOFPLY.js → PanelModelFiles-7HPXWONN.js} +2 -2
  9. package/dist/{PanelModelFiles-NYCOFPLY.js.map → PanelModelFiles-7HPXWONN.js.map} +1 -1
  10. package/dist/PanelModelFiles-JZFSF7BF.mjs +2 -0
  11. package/dist/PanelModelFiles-JZFSF7BF.mjs.map +1 -0
  12. package/dist/analytics.d.mts +2 -2
  13. package/dist/analytics.d.ts +2 -2
  14. package/dist/analytics.js +1 -1
  15. package/dist/analytics.mjs +1 -1
  16. package/dist/catalog.css +1 -1
  17. package/dist/catalog.css.map +1 -1
  18. package/dist/catalog.d.mts +3 -1
  19. package/dist/catalog.d.ts +3 -1
  20. package/dist/catalog.js +1 -1
  21. package/dist/catalog.mjs +1 -1
  22. package/dist/{chunk-GXUNRJ5S.js → chunk-4HGZZJEL.js} +2 -2
  23. package/dist/{chunk-GXUNRJ5S.js.map → chunk-4HGZZJEL.js.map} +1 -1
  24. package/dist/chunk-4OVOKGGS.mjs +2 -0
  25. package/dist/chunk-4OVOKGGS.mjs.map +1 -0
  26. package/dist/chunk-5X7TP3TQ.mjs +2 -0
  27. package/dist/{chunk-SULHGFYN.mjs.map → chunk-5X7TP3TQ.mjs.map} +1 -1
  28. package/dist/{chunk-QQQ2JSPQ.mjs → chunk-CF45RDMD.mjs} +2 -2
  29. package/dist/{chunk-QQQ2JSPQ.mjs.map → chunk-CF45RDMD.mjs.map} +1 -1
  30. package/dist/{chunk-GQJDFLSL.js → chunk-DP2NOHCI.js} +2 -2
  31. package/dist/{chunk-GQJDFLSL.js.map → chunk-DP2NOHCI.js.map} +1 -1
  32. package/dist/chunk-EVIDNS22.mjs +2 -0
  33. package/dist/chunk-EVIDNS22.mjs.map +1 -0
  34. package/dist/chunk-G4V2KWTV.js +2 -0
  35. package/dist/chunk-G4V2KWTV.js.map +1 -0
  36. package/dist/{chunk-SXKJ26OA.mjs → chunk-GEBRXFT3.mjs} +2 -2
  37. package/dist/chunk-GWF3MRUP.js +2 -0
  38. package/dist/chunk-GWF3MRUP.js.map +1 -0
  39. package/dist/chunk-JA62KNAQ.js +2 -0
  40. package/dist/chunk-JA62KNAQ.js.map +1 -0
  41. package/dist/chunk-JHJ3ZZF4.mjs +2 -0
  42. package/dist/chunk-JHJ3ZZF4.mjs.map +1 -0
  43. package/dist/{chunk-WKE6ECID.mjs → chunk-UPAOIY4R.mjs} +2 -2
  44. package/dist/chunk-V5ECGBFW.js +5 -0
  45. package/dist/chunk-V5ECGBFW.js.map +1 -0
  46. package/dist/{chunk-SRQOFCMO.js → chunk-VNV5BD4X.js} +2 -2
  47. package/dist/{chunk-SRQOFCMO.js.map → chunk-VNV5BD4X.js.map} +1 -1
  48. package/dist/chunk-WJO5GSY3.mjs +5 -0
  49. package/dist/chunk-WJO5GSY3.mjs.map +1 -0
  50. package/dist/chunk-ZATPBYOA.js +2 -0
  51. package/dist/chunk-ZATPBYOA.js.map +1 -0
  52. package/dist/chunk-ZHTMK5TA.mjs +2 -0
  53. package/dist/chunk-ZHTMK5TA.mjs.map +1 -0
  54. package/dist/course.d.mts +13 -3
  55. package/dist/course.d.ts +13 -3
  56. package/dist/course.js +1 -1
  57. package/dist/course.js.map +1 -1
  58. package/dist/course.mjs +1 -1
  59. package/dist/course.mjs.map +1 -1
  60. package/dist/embed/analytics.js +1 -1
  61. package/dist/index.css +1 -1
  62. package/dist/index.css.map +1 -1
  63. package/dist/index.d.mts +41 -12
  64. package/dist/index.d.ts +41 -12
  65. package/dist/index.js +1 -1
  66. package/dist/index.js.map +1 -1
  67. package/dist/index.mjs +1 -1
  68. package/dist/index.mjs.map +1 -1
  69. package/dist/{instrument-BE8ktMUv.d.mts → instrument-eXaC8hc2.d.mts} +1 -1
  70. package/dist/{instrument-BE8ktMUv.d.ts → instrument-eXaC8hc2.d.ts} +1 -1
  71. package/dist/methods-AULQDKVS.mjs +2 -0
  72. package/dist/methods-OJEZREBK.css +2 -0
  73. package/dist/methods-OJEZREBK.css.map +1 -0
  74. package/dist/methods-PMH5VQ25.js +2 -0
  75. package/dist/{methods-WRONKK3D.js.map → methods-PMH5VQ25.js.map} +1 -1
  76. package/dist/returns.d.mts +19 -10
  77. package/dist/returns.d.ts +19 -10
  78. package/dist/returns.js +1 -1
  79. package/dist/returns.js.map +1 -1
  80. package/dist/returns.mjs +1 -1
  81. package/dist/{texts-tQtAGJCT.d.mts → texts-LGhuQfA9.d.mts} +28 -3
  82. package/dist/{texts-CIEFbJTN.d.ts → texts-m6XQGZ6w.d.ts} +28 -3
  83. package/dist/types-ClFCVzk_.d.ts +121 -0
  84. package/dist/types-D6LsoycW.d.mts +121 -0
  85. package/dist/viewer3d.css +1 -1
  86. package/dist/viewer3d.css.map +1 -1
  87. package/dist/viewer3d.mjs +1 -1
  88. package/package.json +2 -2
  89. package/dist/CartProvider-PDEV6GCV.css +0 -2
  90. package/dist/CartProvider-PDEV6GCV.css.map +0 -1
  91. package/dist/CartProvider-VGZMDVFJ.js +0 -2
  92. package/dist/CartProvider-ZY5OLTAF.mjs +0 -2
  93. package/dist/PanelModelFiles-RV7OHUUZ.mjs +0 -2
  94. package/dist/PanelModelFiles-RV7OHUUZ.mjs.map +0 -1
  95. package/dist/chunk-5AYH23UJ.mjs +0 -4
  96. package/dist/chunk-5AYH23UJ.mjs.map +0 -1
  97. package/dist/chunk-AGYS7Z6G.mjs +0 -2
  98. package/dist/chunk-AGYS7Z6G.mjs.map +0 -1
  99. package/dist/chunk-DPKWYLN7.mjs +0 -2
  100. package/dist/chunk-DPKWYLN7.mjs.map +0 -1
  101. package/dist/chunk-I3QXJJ7U.mjs +0 -2
  102. package/dist/chunk-I3QXJJ7U.mjs.map +0 -1
  103. package/dist/chunk-LL7ASBGY.js +0 -2
  104. package/dist/chunk-LL7ASBGY.js.map +0 -1
  105. package/dist/chunk-OZHXRKMX.js +0 -2
  106. package/dist/chunk-OZHXRKMX.js.map +0 -1
  107. package/dist/chunk-QERYM7RC.js +0 -2
  108. package/dist/chunk-QERYM7RC.js.map +0 -1
  109. package/dist/chunk-QXGK337V.mjs +0 -2
  110. package/dist/chunk-QXGK337V.mjs.map +0 -1
  111. package/dist/chunk-SULHGFYN.mjs +0 -2
  112. package/dist/chunk-WPOXLITY.js +0 -2
  113. package/dist/chunk-WPOXLITY.js.map +0 -1
  114. package/dist/chunk-Y5KWVTXT.js +0 -4
  115. package/dist/chunk-Y5KWVTXT.js.map +0 -1
  116. package/dist/methods-NJEP64AG.mjs +0 -2
  117. package/dist/methods-QAZED7TJ.css +0 -2
  118. package/dist/methods-QAZED7TJ.css.map +0 -1
  119. package/dist/methods-WRONKK3D.js +0 -2
  120. package/dist/types-DP1vxo5E.d.mts +0 -53
  121. package/dist/types-u8VwmC3j.d.ts +0 -53
  122. /package/dist/{CartProvider-ZY5OLTAF.mjs.map → CartProvider-RYAA3JDA.mjs.map} +0 -0
  123. /package/dist/{chunk-SXKJ26OA.mjs.map → chunk-GEBRXFT3.mjs.map} +0 -0
  124. /package/dist/{chunk-WKE6ECID.mjs.map → chunk-UPAOIY4R.mjs.map} +0 -0
  125. /package/dist/{methods-NJEP64AG.mjs.map → methods-AULQDKVS.mjs.map} +0 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/course/PanelCourse.tsx","../src/course/accent.ts","../src/course/analytics.ts","../src/course/context.tsx","../src/course/CourseOutline.tsx","../src/course/logic.ts","../src/course/rpc.ts","../src/gen/proto/public/v1/user_panel_course_pb.ts","../src/course/datasource.ts","../src/course/types.ts","../src/course/LessonPane.tsx","../src/course/LessonSocial.tsx","../src/course/RatingStars.tsx","../src/course/ui.tsx","../src/course/useAsyncResource.ts","../src/course/sanitize.ts","../src/course/VideoPlayer.tsx","../src/course/texts.ts","../src/course/TestPane.tsx"],"sourcesContent":["import { ChevronLeft, List, Play } from \"lucide-react\";\nimport { useCallback, useEffect, useMemo, useRef, useState } from \"react\";\nimport {\n InstrumentProvider,\n useInstrument,\n type AnalyticsOption,\n} from \"../analytics/instrument\";\nimport { usePanelConfig } from \"../panel/PanelProvider\";\nimport type { GetThemeOutput } from \"../panel/types\";\nimport { cn } from \"../lib/utils\";\nimport { Button } from \"../ui/button\";\nimport { accentVars } from \"./accent\";\nimport { useCourseTrack } from \"./analytics\";\nimport { CourseViewProvider, useCourseView } from \"./context\";\nimport { CourseOutline } from \"./CourseOutline\";\nimport { createRpcCourseDataSource } from \"./rpc\";\nimport { LessonPane } from \"./LessonPane\";\nimport {\n fillTemplate,\n findItem,\n flattenItems,\n getAdjacentItems,\n getAutoplayTarget,\n markLessonCompleted,\n resolveInitialItemId,\n type FlatItem,\n} from \"./logic\";\nimport { getCourseTexts, type CourseTexts } from \"./texts\";\nimport { TestPane } from \"./TestPane\";\nimport {\n CourseError,\n type Course,\n type CourseDataSource,\n type ItemRef,\n type Lesson,\n type ProgressSaved,\n} from \"./types\";\nimport { CARD, ErrorBox, LoadingBlock, ProgressBar, errorMessage } from \"./ui\";\nimport { useAsyncResource } from \"./useAsyncResource\";\n\nexport type PanelCourseProps = {\n /** `UserProduct.access_id` of the purchased course. Not needed with `dataSource`. */\n accessId?: string;\n /** Panel session token; read from the panel session when omitted. */\n accessToken?: string;\n /**\n * Replaces the default data source (`UserPanelCourseService` through the\n * panel's transport). The creator preview of the dashboard passes its own.\n */\n dataSource?: CourseDataSource;\n /** Creator preview: shows a badge and never saves progress. */\n preview?: boolean;\n /** The open lesson or test. Controlled together with `onItemChange`. */\n itemId?: string;\n onItemChange?: (itemId: string) => void;\n /** Shows a back button. */\n onBack?: () => void;\n /** Course structure to render right away (server rendering, preview). */\n initialCourse?: Course;\n /** Text overrides; Polish and English defaults by `locale`. */\n texts?: Partial<CourseTexts>;\n /** Defaults to the locale of the library (`setLocale`). */\n locale?: string;\n /** Brand colors; the panel theme (`setTheme`) by default. */\n theme?: GetThemeOutput;\n className?: string;\n /**\n * Usage statistics for your Bitsnap dashboard (course opened, lessons\n * started and completed, video progress, tests, ratings). On by default;\n * `false` turns it off, and the creator `preview` never reports. Inside\n * `<Panel>` the panel's setting applies unless this is given. An object\n * lets you forward every event: `{ onEvent }`. Nothing is stored on the\n * visitor's device.\n */\n analytics?: AnalyticsOption;\n};\n\nconst AUTOPLAY_KEY = \"bitsnap-course-autoplay\";\n\nfunction useAutoplayPreference(): [boolean, (value: boolean) => void] {\n const [value, setValue] = useState(false);\n useEffect(() => {\n try {\n setValue(window.localStorage.getItem(AUTOPLAY_KEY) === \"1\");\n } catch {\n // Storage is not available (private mode); keep the default.\n }\n }, []);\n const update = useCallback((next: boolean) => {\n setValue(next);\n try {\n window.localStorage.setItem(AUTOPLAY_KEY, next ? \"1\" : \"0\");\n } catch {\n // Not persisted.\n }\n }, []);\n return [value, update];\n}\n\nfunction toRef(item: FlatItem | undefined): ItemRef | undefined {\n return item == null ? undefined : { id: item.id, kind: item.kind };\n}\n\nfunction LockedPane({ item }: { item: FlatItem }) {\n const { texts } = useCourseView();\n return (\n <div className={cn(CARD, \"space-y-2\")} role=\"status\">\n <h2 className=\"text-2xl font-semibold break-words\">{item.title}</h2>\n <p className=\"text-sm text-neutral-600 dark:text-neutral-300\">\n {item.lockReason\n ? fillTemplate(texts.lockedPage, { test: item.lockReason.gatingTestTitle })\n : texts.locked}\n </p>\n </div>\n );\n}\n\ntype LessonContainerProps = {\n course: Course;\n item: FlatItem;\n autoplay: boolean;\n onAutoplayChange: (value: boolean) => void;\n startPlaying: boolean;\n onNavigate: (ref: ItemRef) => void;\n onProgressSaved: (lessonId: string, saved: ProgressSaved) => void;\n onEnded: () => void;\n onLocked: () => void;\n};\n\nfunction LessonContainer({\n course,\n item,\n autoplay,\n onAutoplayChange,\n startPlaying,\n onNavigate,\n onProgressSaved,\n onEnded,\n onLocked,\n}: LessonContainerProps) {\n const { dataSource, texts } = useCourseView();\n const lesson = useAsyncResource<Lesson>(\n () => dataSource.getLesson(item.id),\n [dataSource, item.id],\n );\n\n const lockedByServer =\n lesson.status === \"error\" &&\n lesson.error instanceof CourseError &&\n lesson.error.code === \"item_locked\";\n useEffect(() => {\n // The structure on screen is stale: take the locks from the server again.\n if (lockedByServer) onLocked();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [lockedByServer]);\n\n if (lesson.status === \"loading\") {\n return <LoadingBlock />;\n }\n if (lesson.status === \"error\") {\n return lockedByServer ? (\n <div className={cn(CARD, \"space-y-2\")} role=\"alert\">\n <h2 className=\"text-2xl font-semibold break-words\">{item.title}</h2>\n <p className=\"text-sm\">{errorMessage(lesson.error, texts)}</p>\n </div>\n ) : (\n <ErrorBox error={lesson.error} onRetry={lesson.reload} />\n );\n }\n\n const adjacent = getAdjacentItems(course, item.id);\n const position =\n flattenItems(course)\n .filter((entry) => entry.kind !== \"test\")\n .findIndex((entry) => entry.id === item.id) + 1;\n return (\n <LessonPane\n key={lesson.data.id}\n lesson={lesson.data}\n position={position}\n courseType={course.type}\n prev={lesson.data.prev ?? toRef(adjacent.prev)}\n next={lesson.data.next ?? toRef(adjacent.next)}\n autoplay={autoplay}\n onAutoplayChange={onAutoplayChange}\n startPlaying={startPlaying}\n onNavigate={onNavigate}\n onProgressSaved={onProgressSaved}\n onEnded={onEnded}\n onUrlsExpired={lesson.refresh}\n />\n );\n}\n\nfunction CourseView({\n course,\n selectedId,\n onSelect,\n onBack,\n onCourseChange,\n onCourseRefresh,\n}: {\n course: Course;\n selectedId?: string;\n onSelect: (itemId: string) => void;\n onBack?: () => void;\n onCourseChange: (update: (course: Course) => Course) => void;\n onCourseRefresh: () => void;\n}) {\n const { texts, preview } = useCourseView();\n const track = useCourseTrack();\n const rootRef = useRef<HTMLDivElement>(null);\n const [contentsOpen, setContentsOpen] = useState(false);\n const [autoplay, setAutoplay] = useAutoplayPreference();\n const [playOnOpen, setPlayOnOpen] = useState<string>();\n\n const currentId = resolveInitialItemId(course, selectedId);\n const item = findItem(course, currentId);\n\n // Once per opening of the course.\n useEffect(() => {\n track(\"course_opened\", {});\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [course.id]);\n\n const open = (itemId: string, play = false) => {\n setPlayOnOpen(play ? itemId : undefined);\n setContentsOpen(false);\n onSelect(itemId);\n rootRef.current?.scrollIntoView({ block: \"start\", behavior: \"smooth\" });\n };\n\n const outline = (\n <CourseOutline\n course={course}\n selectedId={currentId}\n variant={course.type === \"video\" ? \"playlist\" : \"modules\"}\n onSelect={(entry) => open(entry.id)}\n />\n );\n\n let content;\n if (item == null) {\n content = <p className=\"text-sm text-neutral-500\">{texts.noItem}</p>;\n } else if (item.locked) {\n content = <LockedPane item={item} />;\n } else if (item.kind === \"test\") {\n content = (\n <TestPane\n key={item.id}\n testId={item.id}\n title={item.title}\n onSubmitted={() => onCourseRefresh()}\n />\n );\n } else {\n content = (\n <LessonContainer\n key={item.id}\n course={course}\n item={item}\n autoplay={autoplay}\n onAutoplayChange={setAutoplay}\n startPlaying={playOnOpen === item.id}\n onNavigate={(ref) => open(ref.id)}\n onProgressSaved={(lessonId, saved) =>\n onCourseChange((current) => ({\n ...(saved.completed ? markLessonCompleted(current, lessonId) : current),\n completedLessons: saved.completedLessons,\n progressPercent: saved.progressPercent,\n }))\n }\n onEnded={() => {\n if (!autoplay) return;\n const target = getAutoplayTarget(course, item.id);\n if (target) open(target.id, true);\n }}\n onLocked={onCourseRefresh}\n />\n );\n }\n\n const isVideo = course.type === \"video\";\n const showResume =\n course.resumeItemId !== \"\" &&\n course.resumeItemId !== currentId &&\n findItem(course, course.resumeItemId)?.locked === false;\n\n return (\n <div ref={rootRef} className=\"space-y-4 scroll-mt-4\">\n <header className=\"space-y-3\">\n <div className=\"flex flex-wrap items-center gap-2\">\n {onBack && (\n <Button variant=\"ghost\" size=\"sm\" onClick={onBack}>\n <ChevronLeft aria-hidden />\n {texts.back}\n </Button>\n )}\n {preview && (\n <span\n data-course-preview\n className=\"rounded-full bg-amber-100 px-3 py-0.5 text-xs font-semibold text-amber-900 dark:bg-amber-900/40 dark:text-amber-200\"\n >\n {texts.previewBadge}\n </span>\n )}\n </div>\n <div className=\"flex flex-wrap items-end justify-between gap-4\">\n <div className=\"min-w-0 space-y-1\">\n <h1 className=\"text-2xl font-semibold break-words sm:text-3xl\">\n {course.title}\n </h1>\n </div>\n {showResume && (\n <Button size=\"sm\" variant=\"outline\" onClick={() => open(course.resumeItemId)}>\n <Play aria-hidden />\n {texts.continueCourse}\n </Button>\n )}\n </div>\n <div className=\"max-w-xl space-y-1\">\n <ProgressBar\n percent={course.progressPercent}\n label={fillTemplate(texts.progressPercent, {\n percent: course.progressPercent,\n })}\n />\n <p className=\"text-xs text-neutral-500 dark:text-neutral-400\">\n {fillTemplate(texts.progressLessons, {\n done: course.completedLessons,\n total: course.totalLessons,\n })}\n {\" · \"}\n {fillTemplate(texts.progressPercent, { percent: course.progressPercent })}\n </p>\n </div>\n </header>\n\n <div\n className={cn(\n \"grid gap-6\",\n isVideo\n ? \"lg:grid-cols-[minmax(0,1fr)_380px]\"\n : \"lg:grid-cols-[320px_minmax(0,1fr)]\",\n )}\n >\n {!isVideo && (\n <div className=\"lg:hidden\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n aria-expanded={contentsOpen}\n onClick={() => setContentsOpen((value) => !value)}\n >\n <List aria-hidden />\n {texts.contents}\n </Button>\n </div>\n )}\n\n {!isVideo && (\n <aside\n className={cn(\n CARD,\n \"p-3 sm:p-3 lg:sticky lg:top-4 lg:block lg:max-h-[calc(100vh-2rem)] lg:self-start lg:overflow-y-auto\",\n contentsOpen ? \"block\" : \"hidden\",\n )}\n >\n {outline}\n </aside>\n )}\n\n <div className=\"min-w-0\" data-course-content>\n {content}\n </div>\n\n {isVideo && (\n <aside\n className={cn(\n CARD,\n \"p-3 sm:p-3 lg:sticky lg:top-4 lg:max-h-[calc(100vh-2rem)] lg:self-start lg:overflow-y-auto\",\n )}\n >\n {outline}\n </aside>\n )}\n </div>\n </div>\n );\n}\n\n/**\n * The course of a customer inside the panel: structure with progress and\n * locks, lessons with video, text and attachments, tests, questions and\n * ratings. Standard courses get a sidebar of modules, video courses a\n * playlist next to the player.\n *\n * Load it lazily from `bitsnap-react/course`. Inside the panel it needs only\n * `accessId` of the purchased course.\n */\nexport function PanelCourse({\n accessId,\n accessToken,\n dataSource: customDataSource,\n preview = false,\n itemId,\n onItemChange,\n onBack,\n initialCourse,\n texts: textOverrides,\n locale,\n theme,\n className,\n analytics,\n}: PanelCourseProps) {\n const panel = usePanelConfig();\n // The creator preview reports nothing, not even the page view of the page it sits on.\n const instrument = useInstrument(panel.projectID, preview ? false : analytics, \"course\");\n const texts = useMemo(\n () => getCourseTexts(textOverrides, locale),\n [textOverrides, locale],\n );\n const dataSource = useMemo(() => {\n if (customDataSource) return customDataSource;\n if (accessId == null || accessId === \"\") {\n throw new Error(\"PanelCourse needs an accessId or a dataSource\");\n }\n return createRpcCourseDataSource({ accessId, accessToken });\n }, [customDataSource, accessId, accessToken]);\n\n const course = useAsyncResource<Course>(\n () => dataSource.getCourse(),\n [dataSource],\n initialCourse,\n );\n\n const [internalItemId, setInternalItemId] = useState<string>();\n // With `onItemChange` the owner navigates, so no `itemId` means the default\n // item, not the one picked last.\n const selectedId = onItemChange ? itemId : (itemId ?? internalItemId);\n const select = useCallback(\n (next: string) => {\n setInternalItemId(next);\n onItemChange?.(next);\n },\n [onItemChange],\n );\n\n const style = accentVars(theme?.colors ?? panel.theme?.colors);\n\n const courseId = course.status === \"success\" ? course.data.id : undefined;\n const viewValue = useMemo(\n () => ({\n dataSource,\n texts,\n preview,\n analytics: { courseId, instrument },\n }),\n [dataSource, texts, preview, courseId, instrument],\n );\n\n return (\n <InstrumentProvider value={instrument}>\n <CourseViewProvider value={viewValue}>\n <div\n data-bitsnap-course\n data-course-type={course.status === \"success\" ? course.data.type : undefined}\n style={style}\n className={cn(\"font-inter w-full max-w-7xl\", className)}\n >\n {course.status === \"loading\" && (\n <div className=\"space-y-4\">\n {onBack && (\n <Button variant=\"ghost\" size=\"sm\" onClick={onBack}>\n <ChevronLeft aria-hidden />\n {texts.back}\n </Button>\n )}\n <p className=\"sr-only\" role=\"status\">\n {texts.loading}\n </p>\n <LoadingBlock />\n </div>\n )}\n {course.status === \"error\" && (\n <div className=\"space-y-4\">\n {onBack && (\n <Button variant=\"ghost\" size=\"sm\" onClick={onBack}>\n <ChevronLeft aria-hidden />\n {texts.back}\n </Button>\n )}\n <ErrorBox error={course.error} onRetry={course.reload} />\n </div>\n )}\n {course.status === \"success\" && (\n <CourseView\n course={course.data}\n selectedId={selectedId}\n onSelect={select}\n onBack={onBack}\n onCourseChange={course.mutate}\n onCourseRefresh={course.refresh}\n />\n )}\n </div>\n </CourseViewProvider>\n </InstrumentProvider>\n );\n}\n\nexport default PanelCourse;\n","import type { CSSProperties } from \"react\";\nimport { readableForeground } from \"../components/returns/color\";\n\n/**\n * CSS variables of the accent (brand) color of the course view. Without a\n * brand color nothing is set and the library's `--primary` applies.\n */\nexport function accentVars(colors?: {\n brand?: string;\n brandDark?: string;\n}): CSSProperties {\n const vars: Record<string, string> = {};\n const brand = colors?.brand?.trim();\n const brandDark = colors?.brandDark?.trim() || brand;\n if (brand) {\n vars[\"--course-accent\"] = brand;\n const foreground = readableForeground(brand);\n if (foreground) vars[\"--course-accent-fg\"] = foreground;\n }\n if (brandDark) {\n vars[\"--course-accent-dark\"] = brandDark;\n const foreground = readableForeground(brandDark);\n if (foreground) vars[\"--course-accent-dark-fg\"] = foreground;\n }\n return vars as CSSProperties;\n}\n","import { useCallback } from \"react\";\nimport type { EventProps } from \"@/src/analytics/contract\";\nimport { useCourseView } from \"./context\";\n\ntype CourseEventName =\n | \"course_opened\"\n | \"lesson_started\"\n | \"lesson_completed\"\n | \"video_progress\"\n | \"test_submitted\"\n | \"lesson_rated\";\n\n/**\n * Reports a course event; `course_id` is added. Nothing is reported in the\n * creator preview, before the course has loaded, or without analytics.\n */\nexport function useCourseTrack() {\n const { analytics, preview } = useCourseView();\n return useCallback(\n <E extends CourseEventName>(\n event: E,\n props: Omit<EventProps<E>, \"course_id\">,\n ) => {\n if (preview || analytics?.courseId == null) {\n return;\n }\n analytics.instrument.track(event, {\n ...props,\n course_id: analytics.courseId,\n } as unknown as EventProps<E>);\n },\n [analytics, preview],\n );\n}\n","import { createContext, useContext } from \"react\";\nimport type { Instrument } from \"@/src/analytics/instrument\";\nimport type { CourseTexts } from \"./texts\";\nimport type { CourseDataSource } from \"./types\";\n\nexport type CourseAnalytics = {\n /** `undefined` until the course has loaded. */\n courseId?: string;\n instrument: Instrument;\n};\n\nexport type CourseViewContextValue = {\n dataSource: CourseDataSource;\n texts: CourseTexts;\n /** Creator preview: nothing the viewer does is saved as progress. */\n preview: boolean;\n /** Usage statistics; none without it. */\n analytics?: CourseAnalytics;\n};\n\nconst CourseViewContext = createContext<CourseViewContextValue | undefined>(\n undefined,\n);\n\nexport const CourseViewProvider = CourseViewContext.Provider;\n\nexport function useCourseView(): CourseViewContextValue {\n const value = useContext(CourseViewContext);\n if (value == null) {\n throw new Error(\"Course components have to be rendered inside PanelCourse\");\n }\n return value;\n}\n","import {\n CheckCircle2,\n ClipboardCheck,\n Lock,\n PlayCircle,\n FileText,\n} from \"lucide-react\";\nimport { cn } from \"../lib/utils\";\nimport { useCourseView } from \"./context\";\nimport { fillTemplate, formatDuration } from \"./logic\";\nimport type { Course, CourseItem } from \"./types\";\n\n\ntype CourseOutlineProps = {\n course: Course;\n selectedId?: string;\n onSelect: (item: CourseItem) => void;\n /**\n * `playlist` (video courses) shows the thumbnail and duration of every\n * video; `modules` (standard courses) is a plain list of lessons.\n */\n variant: \"modules\" | \"playlist\";\n className?: string;\n};\n\nfunction ItemIcon({ item }: { item: CourseItem }) {\n if (item.locked) {\n return <Lock className=\"size-4 text-neutral-500\" aria-hidden />;\n }\n if (item.completed) {\n return (\n <CheckCircle2\n className=\"size-4 text-emerald-600 dark:text-emerald-400\"\n aria-hidden\n />\n );\n }\n if (item.kind === \"test\") {\n return <ClipboardCheck className=\"size-4 text-neutral-500\" aria-hidden />;\n }\n return item.durationSeconds > 0 ? (\n <PlayCircle className=\"size-4 text-neutral-500\" aria-hidden />\n ) : (\n <FileText className=\"size-4 text-neutral-500\" aria-hidden />\n );\n}\n\nfunction OutlineItem({\n item,\n selected,\n variant,\n onSelect,\n}: {\n item: CourseItem;\n selected: boolean;\n variant: \"modules\" | \"playlist\";\n onSelect: (item: CourseItem) => void;\n}) {\n const { texts } = useCourseView();\n const lockText = item.lockReason\n ? fillTemplate(texts.lockedByTest, { test: item.lockReason.gatingTestTitle })\n : texts.locked;\n const state = item.locked\n ? lockText\n : item.completed\n ? texts.completed\n : undefined;\n\n return (\n <li>\n <button\n type=\"button\"\n data-item-id={item.id}\n data-locked={item.locked || undefined}\n aria-current={selected ? \"true\" : undefined}\n aria-disabled={item.locked || undefined}\n title={item.locked ? lockText : undefined}\n onClick={() => {\n if (!item.locked) onSelect(item);\n }}\n className={cn(\n \"flex w-full items-start gap-3 rounded-lg p-2 text-left text-sm outline-none transition focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n selected\n ? \"bg-neutral-200 dark:bg-neutral-700\"\n : \"hover:bg-neutral-100 dark:hover:bg-neutral-800\",\n item.locked && \"cursor-not-allowed opacity-60 hover:bg-transparent dark:hover:bg-transparent\",\n )}\n >\n {variant === \"playlist\" && item.kind === \"lesson\" && (\n <span className=\"relative aspect-video w-24 shrink-0 overflow-hidden rounded bg-neutral-200 dark:bg-neutral-800\">\n {item.thumbnailUrl && (\n <img\n src={item.thumbnailUrl}\n alt=\"\"\n loading=\"lazy\"\n draggable={false}\n className=\"size-full object-cover\"\n />\n )}\n {item.durationSeconds > 0 && (\n <span className=\"absolute right-1 bottom-1 rounded bg-black/75 px-1 text-[10px] text-white\">\n {formatDuration(item.durationSeconds)}\n </span>\n )}\n </span>\n )}\n {(variant === \"modules\" || item.kind === \"test\") && (\n <span className=\"mt-0.5 shrink-0\">\n <ItemIcon item={item} />\n </span>\n )}\n <span className=\"min-w-0 flex-1\">\n <span className=\"block font-medium break-words\">{item.title}</span>\n <span className=\"mt-0.5 flex flex-wrap items-center gap-x-2 text-xs text-neutral-500 dark:text-neutral-400\">\n {item.kind === \"test\" && <span>{texts.testBadge}</span>}\n {item.isFree && !item.locked && <span>{texts.free}</span>}\n {variant === \"modules\" && item.durationSeconds > 0 && (\n <span>{formatDuration(item.durationSeconds)}</span>\n )}\n {state && <span>{state}</span>}\n </span>\n </span>\n {variant === \"playlist\" && item.kind === \"lesson\" && (\n <span className=\"mt-0.5 shrink-0\">\n <ItemIcon item={item} />\n </span>\n )}\n </button>\n </li>\n );\n}\n\n/** Modules / sections with their lessons and tests. */\nexport function CourseOutline({\n course,\n selectedId,\n onSelect,\n variant,\n className,\n}: CourseOutlineProps) {\n const { texts } = useCourseView();\n\n const renderItems = (items: CourseItem[]) => (\n <ul className=\"flex flex-col gap-1\">\n {items.map((item) => (\n <OutlineItem\n key={item.id}\n item={item}\n selected={item.id === selectedId}\n variant={variant}\n onSelect={onSelect}\n />\n ))}\n </ul>\n );\n\n return (\n <nav aria-label={texts.contents} className={cn(\"space-y-4\", className)}>\n {course.sections.map((section) => {\n const lessons = section.items.filter((i) => i.kind === \"lesson\");\n const done = lessons.filter((i) => i.completed).length;\n return (\n <section key={section.id} data-section-id={section.id}>\n <h3 className=\"mb-1 flex items-baseline justify-between gap-2 px-2 text-sm font-semibold\">\n <span className=\"break-words\">{section.title}</span>\n {lessons.length > 0 && (\n <span className=\"shrink-0 text-xs font-normal text-neutral-500 dark:text-neutral-400\">\n {done}/{lessons.length}\n </span>\n )}\n </h3>\n {renderItems(section.items)}\n </section>\n );\n })}\n {course.finalItems.length > 0 && (\n <section data-section-id=\"final\">{renderItems(course.finalItems)}</section>\n )}\n </nav>\n );\n}\n\n","import type {\n Course,\n CourseItem,\n QuestionAnswer,\n TestAnswers,\n TestQuestion,\n TestQuestionConfig,\n} from \"./types\";\n\n/** Playback share from which a lesson counts as watched. */\nexport const COMPLETE_AT_FRACTION = 0.8;\n\n/** How often the player reports the position while it plays. */\nexport const PROGRESS_SAVE_INTERVAL_MS = 15_000;\n\n/** An item together with the section it sits in (empty for final tests). */\nexport type FlatItem = CourseItem & { sectionId: string };\n\n/** All items of the course in the order they are taken. */\nexport function flattenItems(course: Course): FlatItem[] {\n return [\n ...course.sections.flatMap((section) =>\n section.items.map((item) => ({ ...item, sectionId: section.id })),\n ),\n ...course.finalItems.map((item) => ({ ...item, sectionId: \"\" })),\n ];\n}\n\nexport function findItem(\n course: Course,\n itemId: string | undefined,\n): FlatItem | undefined {\n if (itemId == null || itemId === \"\") {\n return undefined;\n }\n return flattenItems(course).find((item) => item.id === itemId);\n}\n\nexport function getAdjacentItems(\n course: Course,\n itemId: string,\n): { prev?: FlatItem; next?: FlatItem } {\n const items = flattenItems(course);\n const index = items.findIndex((item) => item.id === itemId);\n if (index < 0) {\n return {};\n }\n return { prev: items[index - 1], next: items[index + 1] };\n}\n\n/**\n * The item to start autoplay with after `itemId` ended: the next item when it\n * is an unlocked lesson. A test or a locked lesson stops the playlist.\n */\nexport function getAutoplayTarget(\n course: Course,\n itemId: string,\n): FlatItem | undefined {\n const { next } = getAdjacentItems(course, itemId);\n return next != null && next.kind === \"lesson\" && !next.locked\n ? next\n : undefined;\n}\n\n/** Progress counted from the lessons of the structure (tests do not count). */\nexport function computeProgress(course: Pick<Course, \"sections\">): {\n total: number;\n completed: number;\n percent: number;\n} {\n let total = 0;\n let completed = 0;\n for (const section of course.sections) {\n for (const item of section.items) {\n if (item.kind !== \"lesson\") {\n continue;\n }\n total += 1;\n if (item.completed) {\n completed += 1;\n }\n }\n }\n return {\n total,\n completed,\n percent: total === 0 ? 0 : Math.round((completed / total) * 100),\n };\n}\n\n/** The course after a lesson was completed, progress recomputed. */\nexport function markLessonCompleted(course: Course, lessonId: string): Course {\n const sections = course.sections.map((section) => ({\n ...section,\n items: section.items.map((item) =>\n item.id === lessonId && item.kind === \"lesson\"\n ? { ...item, completed: true }\n : item,\n ),\n }));\n const progress = computeProgress({ sections });\n return {\n ...course,\n sections,\n totalLessons: progress.total,\n completedLessons: progress.completed,\n progressPercent: progress.percent,\n };\n}\n\n/**\n * Item to show first: the requested one when it exists, else the one to\n * resume with, else the first unlocked item.\n */\nexport function resolveInitialItemId(\n course: Course,\n requestedId?: string,\n): string | undefined {\n if (findItem(course, requestedId) != null) {\n return requestedId;\n }\n if (findItem(course, course.resumeItemId)?.locked === false) {\n return course.resumeItemId;\n }\n return flattenItems(course).find((item) => !item.locked)?.id;\n}\n\n/** `{name}` placeholders of a text. Unknown placeholders stay untouched. */\nexport function fillTemplate(\n template: string,\n values: Record<string, string | number>,\n): string {\n return template.replace(/\\{(\\w+)\\}/g, (match, key: string) =>\n key in values ? String(values[key]) : match,\n );\n}\n\nexport function isPlaybackComplete(\n positionSeconds: number,\n durationSeconds: number,\n): boolean {\n return (\n durationSeconds > 0 &&\n positionSeconds / durationSeconds >= COMPLETE_AT_FRACTION\n );\n}\n\n/** Whether a periodic save is due. */\nexport function isSaveDue(\n lastSavedAt: number | undefined,\n now: number,\n intervalMs: number = PROGRESS_SAVE_INTERVAL_MS,\n): boolean {\n return lastSavedAt == null || now - lastSavedAt >= intervalMs;\n}\n\n/** Where to resume: nothing when the video was (nearly) watched to the end. */\nexport function resumePosition(\n positionSeconds: number,\n durationSeconds: number,\n): number {\n if (positionSeconds < 5) {\n return 0;\n }\n if (durationSeconds > 0 && positionSeconds >= durationSeconds - 5) {\n return 0;\n }\n return Math.floor(positionSeconds);\n}\n\n/** `m:ss`, or `h:mm:ss` from an hour on. */\nexport function formatDuration(totalSeconds: number): string {\n const seconds = Math.max(0, Math.floor(totalSeconds));\n const h = Math.floor(seconds / 3600);\n const m = Math.floor((seconds % 3600) / 60);\n const s = String(seconds % 60).padStart(2, \"0\");\n return h > 0 ? `${h}:${String(m).padStart(2, \"0\")}:${s}` : `${m}:${s}`;\n}\n\nexport function formatBytes(bytes: number): string {\n if (!(bytes > 0)) {\n return \"0 B\";\n }\n const units = [\"B\", \"KB\", \"MB\", \"GB\"];\n const exponent = Math.min(\n Math.floor(Math.log(bytes) / Math.log(1024)),\n units.length - 1,\n );\n const value = bytes / 1024 ** exponent;\n return `${exponent === 0 || value >= 10 ? Math.round(value) : value.toFixed(1)} ${units[exponent]}`;\n}\n\n// Tests\n\n/** The empty answer of a question, with the order of sort items as given. */\nexport function emptyAnswer(config: TestQuestionConfig): QuestionAnswer {\n switch (config.type) {\n case \"mcq\":\n return { type: \"mcq\", selectedIds: [] };\n case \"todolist\":\n return { type: \"todolist\", checkedIds: [] };\n case \"fill_gap\":\n return { type: \"fill_gap\", values: {} };\n case \"sort_order\":\n return {\n type: \"sort_order\",\n orderedIds: config.items.map((item) => item.id),\n };\n }\n}\n\n/** Whether the customer did anything with the question. */\nexport function isAnswered(\n question: TestQuestion,\n answer: QuestionAnswer | undefined,\n): boolean {\n if (answer == null || answer.type !== question.config.type) {\n return false;\n }\n switch (answer.type) {\n case \"mcq\":\n return answer.selectedIds.length > 0;\n case \"todolist\":\n return answer.checkedIds.length > 0;\n case \"fill_gap\":\n return Object.values(answer.values).some((value) => value.trim() !== \"\");\n case \"sort_order\":\n // The view stores a sort answer once the customer moved an item.\n return answer.orderedIds.length > 0;\n }\n}\n\nexport function countUnanswered(\n questions: TestQuestion[],\n answers: TestAnswers,\n): number {\n return questions.filter((q) => !isAnswered(q, answers[q.id])).length;\n}\n\n/** Moves the element at `from` to `to`; out of range moves change nothing. */\nexport function moveItem<T>(list: readonly T[], from: number, to: number): T[] {\n if (\n from === to ||\n from < 0 ||\n to < 0 ||\n from >= list.length ||\n to >= list.length\n ) {\n return [...list];\n }\n const copy = [...list];\n const [item] = copy.splice(from, 1);\n copy.splice(to, 0, item!);\n return copy;\n}\n\nexport type GapSegment =\n | { type: \"text\"; text: string }\n | { type: \"blank\"; id: string };\n\n/**\n * Splits a fill-in-the-gap template (`Paris is in {{g1}}`) into text and\n * blanks. Placeholders that are not in `blanks` stay text.\n */\nexport function splitGapTemplate(\n template: string,\n blanks: readonly string[],\n): GapSegment[] {\n const segments: GapSegment[] = [];\n const pattern = /\\{\\{\\s*([^{}\\s]+)\\s*\\}\\}/g;\n let last = 0;\n for (const match of template.matchAll(pattern)) {\n const id = match[1]!;\n if (!blanks.includes(id)) {\n continue;\n }\n if (match.index > last) {\n segments.push({ type: \"text\", text: template.slice(last, match.index) });\n }\n segments.push({ type: \"blank\", id });\n last = match.index + match[0].length;\n }\n if (last < template.length) {\n segments.push({ type: \"text\", text: template.slice(last) });\n }\n return segments;\n}\n\n/** A readable form of an answer, used on the result screen. */\nexport function describeAnswer(\n question: TestQuestion,\n answer: QuestionAnswer | undefined,\n): string {\n const { config } = question;\n if (answer == null || answer.type !== config.type) {\n return \"\";\n }\n const label = (list: { id: string; label: string }[], id: string) =>\n list.find((entry) => entry.id === id)?.label ?? id;\n\n if (config.type === \"mcq\" && answer.type === \"mcq\") {\n return answer.selectedIds.map((id) => label(config.choices, id)).join(\", \");\n }\n if (config.type === \"todolist\" && answer.type === \"todolist\") {\n return answer.checkedIds.map((id) => label(config.steps, id)).join(\", \");\n }\n if (config.type === \"sort_order\" && answer.type === \"sort_order\") {\n return answer.orderedIds.map((id) => label(config.items, id)).join(\" → \");\n }\n if (config.type === \"fill_gap\" && answer.type === \"fill_gap\") {\n return splitGapTemplate(config.template, config.blanks)\n .map((segment) =>\n segment.type === \"text\"\n ? segment.text\n : `[${answer.values[segment.id] ?? \"\"}]`,\n )\n .join(\"\");\n }\n return \"\";\n}\n","import { createClient } from \"@connectrpc/connect\";\nimport { getPanelConfig } from \"../panel/config\";\nimport { getPanelSession } from \"../panel/session-storage\";\nimport { getRpcTransport, getTransportHost } from \"../rpc-provider\";\nimport { UserPanelCourseService } from \"../gen/proto/public/v1/user_panel_course_pb\";\nimport { createCourseDataSource } from \"./datasource\";\nimport type { CourseDataSource } from \"./types\";\n\nexport type RpcCourseDataSourceOptions = {\n /** `UserProduct.access_id` of the purchased course. */\n accessId: string;\n /**\n * Panel session token. By default it is read from the panel session of the\n * configured project (`setProjectID`, `setHost`) on every call.\n */\n accessToken?: string;\n /** Bitsnap host; the panel host (`setHost`, bitsnap.pl by default) otherwise. */\n host?: string;\n};\n\n/**\n * Default data source: `UserPanelCourseService` on `{host}/api/rpc`, using the\n * transport and the session token of the panel.\n */\nexport function createRpcCourseDataSource(\n options: RpcCourseDataSourceOptions,\n): CourseDataSource {\n const host = options.host ?? getTransportHost();\n const client = createClient(UserPanelCourseService, getRpcTransport(host));\n return createCourseDataSource(client, {\n accessId: options.accessId,\n accessToken:\n options.accessToken ??\n (() => {\n const { projectID, host: panelHost } = getPanelConfig();\n return getPanelSession(projectID, panelHost);\n }),\n });\n}\n","// @generated by protoc-gen-es v2.10.0 with parameter \"target=ts,ts_nocheck=true\"\n// @generated from file public/v1/user_panel_course.proto (package public_api.v1, syntax proto3)\n/* eslint-disable */\n// @ts-nocheck\n\nimport type { GenEnum, GenFile, GenMessage, GenService } from \"@bufbuild/protobuf/codegenv2\";\nimport { enumDesc, fileDesc, messageDesc, serviceDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport type { Timestamp } from \"@bufbuild/protobuf/wkt\";\nimport { file_google_protobuf_timestamp } from \"@bufbuild/protobuf/wkt\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file public/v1/user_panel_course.proto.\n */\nexport const file_public_v1_user_panel_course: GenFile = /*@__PURE__*/\n fileDesc(\"CiFwdWJsaWMvdjEvdXNlcl9wYW5lbF9jb3Vyc2UucHJvdG8SDXB1YmxpY19hcGkudjEiRAoPUGFuZWxMb2NrUmVhc29uEhYKDmdhdGluZ190ZXN0X2lkGAEgASgJEhkKEWdhdGluZ190ZXN0X3RpdGxlGAIgASgJIrQCCg9QYW5lbENvdXJzZUl0ZW0SCgoCaWQYASABKAkSMAoEa2luZBgCIAEoDjIiLnB1YmxpY19hcGkudjEuUGFuZWxDb3Vyc2VJdGVtS2luZBINCgV0aXRsZRgDIAEoCRIPCgdpc19mcmVlGAQgASgIEg4KBmxvY2tlZBgFIAEoCBI4Cgtsb2NrX3JlYXNvbhgGIAEoCzIeLnB1YmxpY19hcGkudjEuUGFuZWxMb2NrUmVhc29uSACIAQESEQoJY29tcGxldGVkGAcgASgIEhgKEGR1cmF0aW9uX3NlY29uZHMYCCABKAUSGgoNdGh1bWJuYWlsX3VybBgJIAEoCUgBiAEBEg4KBmdhdGluZxgKIAEoCEIOCgxfbG9ja19yZWFzb25CEAoOX3RodW1ibmFpbF91cmwicwoSUGFuZWxDb3Vyc2VTZWN0aW9uEgoKAmlkGAEgASgJEg0KBXRpdGxlGAIgASgJEhMKC2Rlc2NyaXB0aW9uGAMgASgJEi0KBWl0ZW1zGAQgAygLMh4ucHVibGljX2FwaS52MS5QYW5lbENvdXJzZUl0ZW0i3wIKC1BhbmVsQ291cnNlEgoKAmlkGAEgASgJEiwKBHR5cGUYAiABKA4yHi5wdWJsaWNfYXBpLnYxLlBhbmVsQ291cnNlVHlwZRINCgV0aXRsZRgDIAEoCRITCgtkZXNjcmlwdGlvbhgEIAEoCRIWCgljb3Zlcl91cmwYBSABKAlIAIgBARIVCg10b3RhbF9sZXNzb25zGAYgASgFEhkKEWNvbXBsZXRlZF9sZXNzb25zGAcgASgFEhgKEHByb2dyZXNzX3BlcmNlbnQYCCABKAUSMwoIc2VjdGlvbnMYCSADKAsyIS5wdWJsaWNfYXBpLnYxLlBhbmVsQ291cnNlU2VjdGlvbhIzCgtmaW5hbF9pdGVtcxgKIAMoCzIeLnB1YmxpY19hcGkudjEuUGFuZWxDb3Vyc2VJdGVtEhYKDnJlc3VtZV9pdGVtX2lkGAsgASgJQgwKCl9jb3Zlcl91cmwiRAoZVXNlclBhbmVsR2V0Q291cnNlUmVxdWVzdBIUCgxhY2Nlc3NfdG9rZW4YASABKAkSEQoJYWNjZXNzX2lkGAIgASgJIosBChpVc2VyUGFuZWxHZXRDb3Vyc2VSZXNwb25zZRItCgdzdWNjZXNzGAEgASgLMhoucHVibGljX2FwaS52MS5QYW5lbENvdXJzZUgAEjQKB2ZhaWx1cmUYAiABKA4yIS5wdWJsaWNfYXBpLnYxLlBhbmVsQ291cnNlRmFpbHVyZUgAQggKBnJlc3VsdCJfCgpQYW5lbFZpZGVvEg8KB2hsc191cmwYASABKAkSFwoKcG9zdGVyX3VybBgCIAEoCUgAiAEBEhgKEGR1cmF0aW9uX3NlY29uZHMYAyABKAVCDQoLX3Bvc3Rlcl91cmwiRgoPUGFuZWxBdHRhY2htZW50EgoKAmlkGAEgASgJEgwKBG5hbWUYAiABKAkSDAoEc2l6ZRgDIAEoAxILCgN1cmwYBCABKAkiTAoMUGFuZWxJdGVtUmVmEgoKAmlkGAEgASgJEjAKBGtpbmQYAiABKA4yIi5wdWJsaWNfYXBpLnYxLlBhbmVsQ291cnNlSXRlbUtpbmQigQQKC1BhbmVsTGVzc29uEgoKAmlkGAEgASgJEhIKCnNlY3Rpb25faWQYAiABKAkSDQoFdGl0bGUYAyABKAkSEwoLZGVzY3JpcHRpb24YBCABKAkSFAoMY29udGVudF9odG1sGAUgASgJEi0KBXZpZGVvGAYgASgLMhkucHVibGljX2FwaS52MS5QYW5lbFZpZGVvSACIAQESMwoLYXR0YWNobWVudHMYByADKAsyHi5wdWJsaWNfYXBpLnYxLlBhbmVsQXR0YWNobWVudBIuCgRwcmV2GAggASgLMhsucHVibGljX2FwaS52MS5QYW5lbEl0ZW1SZWZIAYgBARIuCgRuZXh0GAkgASgLMhsucHVibGljX2FwaS52MS5QYW5lbEl0ZW1SZWZIAogBARIRCgljb21wbGV0ZWQYCiABKAgSGAoQcG9zaXRpb25fc2Vjb25kcxgLIAEoBRIRCglteV9yYXRpbmcYDCABKAUSFgoOYXZlcmFnZV9yYXRpbmcYDSABKAISFAoMcmF0aW5nX2NvdW50GA4gASgFEjcKDnVybHNfZXhwaXJlX2F0GA8gASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgDiAEBQggKBl92aWRlb0IHCgVfcHJldkIHCgVfbmV4dEIRCg9fdXJsc19leHBpcmVfYXQiVwoZVXNlclBhbmVsR2V0TGVzc29uUmVxdWVzdBIUCgxhY2Nlc3NfdG9rZW4YASABKAkSEQoJYWNjZXNzX2lkGAIgASgJEhEKCWxlc3Nvbl9pZBgDIAEoCSKLAQoaVXNlclBhbmVsR2V0TGVzc29uUmVzcG9uc2USLQoHc3VjY2VzcxgBIAEoCzIaLnB1YmxpY19hcGkudjEuUGFuZWxMZXNzb25IABI0CgdmYWlsdXJlGAIgASgOMiEucHVibGljX2FwaS52MS5QYW5lbENvdXJzZUZhaWx1cmVIAEIICgZyZXN1bHQipwEKIlVzZXJQYW5lbFNhdmVMZXNzb25Qcm9ncmVzc1JlcXVlc3QSFAoMYWNjZXNzX3Rva2VuGAEgASgJEhEKCWFjY2Vzc19pZBgCIAEoCRIRCglsZXNzb25faWQYAyABKAkSEQoJY29tcGxldGVkGAQgASgIEhgKEHBvc2l0aW9uX3NlY29uZHMYBSABKAUSGAoQZHVyYXRpb25fc2Vjb25kcxgGIAEoBSKFAgojVXNlclBhbmVsU2F2ZUxlc3NvblByb2dyZXNzUmVzcG9uc2USTAoHc3VjY2VzcxgBIAEoCzI5LnB1YmxpY19hcGkudjEuVXNlclBhbmVsU2F2ZUxlc3NvblByb2dyZXNzUmVzcG9uc2UuUmVzdWx0SAASNAoHZmFpbHVyZRgCIAEoDjIhLnB1YmxpY19hcGkudjEuUGFuZWxDb3Vyc2VGYWlsdXJlSAAaUAoGUmVzdWx0EhEKCWNvbXBsZXRlZBgBIAEoCBIZChFjb21wbGV0ZWRfbGVzc29ucxgCIAEoBRIYChBwcm9ncmVzc19wZXJjZW50GAMgASgFQggKBnJlc3VsdCIoCgtQYW5lbENob2ljZRIKCgJpZBgBIAEoCRINCgVsYWJlbBgCIAEoCSJPCg5QYW5lbE1jcUNvbmZpZxIrCgdjaG9pY2VzGAEgAygLMhoucHVibGljX2FwaS52MS5QYW5lbENob2ljZRIQCghtdWx0aXBsZRgCIAEoCCKCAQoTUGFuZWxUb2RvbGlzdENvbmZpZxI2CgVzdGVwcxgBIAMoCzInLnB1YmxpY19hcGkudjEuUGFuZWxUb2RvbGlzdENvbmZpZy5TdGVwGjMKBFN0ZXASCgoCaWQYASABKAkSDQoFbGFiZWwYAiABKAkSEAoIcmVxdWlyZWQYAyABKAgiNgoSUGFuZWxGaWxsR2FwQ29uZmlnEhAKCHRlbXBsYXRlGAEgASgJEg4KBmJsYW5rcxgCIAMoCSJBChRQYW5lbFNvcnRPcmRlckNvbmZpZxIpCgVpdGVtcxgBIAMoCzIaLnB1YmxpY19hcGkudjEuUGFuZWxDaG9pY2UioQIKEVBhbmVsVGVzdFF1ZXN0aW9uEgoKAmlkGAEgASgJEg4KBnByb21wdBgCIAEoCRIOCgZwb2ludHMYAyABKAUSLAoDbWNxGAogASgLMh0ucHVibGljX2FwaS52MS5QYW5lbE1jcUNvbmZpZ0gAEjYKCHRvZG9saXN0GAsgASgLMiIucHVibGljX2FwaS52MS5QYW5lbFRvZG9saXN0Q29uZmlnSAASNQoIZmlsbF9nYXAYDCABKAsyIS5wdWJsaWNfYXBpLnYxLlBhbmVsRmlsbEdhcENvbmZpZ0gAEjkKCnNvcnRfb3JkZXIYDSABKAsyIy5wdWJsaWNfYXBpLnYxLlBhbmVsU29ydE9yZGVyQ29uZmlnSABCCAoGY29uZmlnIqsCCglQYW5lbFRlc3QSCgoCaWQYASABKAkSDQoFdGl0bGUYAiABKAkSEwoLZGVzY3JpcHRpb24YAyABKAkSDgoGZ2F0aW5nGAQgASgIEh4KFnBhc3NfdGhyZXNob2xkX3BlcmNlbnQYBSABKAUSGAoQYXR0ZW1wdHNfYWxsb3dlZBgGIAEoBRIaCg1hdHRlbXB0c19sZWZ0GAcgASgFSACIAQESFQoNYXR0ZW1wdF9jb3VudBgIIAEoBRIaChJiZXN0X3Njb3JlX3BlcmNlbnQYCSABKAUSDgoGcGFzc2VkGAogASgIEjMKCXF1ZXN0aW9ucxgLIAMoCzIgLnB1YmxpY19hcGkudjEuUGFuZWxUZXN0UXVlc3Rpb25CEAoOX2F0dGVtcHRzX2xlZnQiUwoXVXNlclBhbmVsR2V0VGVzdFJlcXVlc3QSFAoMYWNjZXNzX3Rva2VuGAEgASgJEhEKCWFjY2Vzc19pZBgCIAEoCRIPCgd0ZXN0X2lkGAMgASgJIocBChhVc2VyUGFuZWxHZXRUZXN0UmVzcG9uc2USKwoHc3VjY2VzcxgBIAEoCzIYLnB1YmxpY19hcGkudjEuUGFuZWxUZXN0SAASNAoHZmFpbHVyZRgCIAEoDjIhLnB1YmxpY19hcGkudjEuUGFuZWxDb3Vyc2VGYWlsdXJlSABCCAoGcmVzdWx0IiYKDlBhbmVsTWNxQW5zd2VyEhQKDHNlbGVjdGVkX2lkcxgBIAMoCSIqChNQYW5lbFRvZG9saXN0QW5zd2VyEhMKC2NoZWNrZWRfaWRzGAEgAygJIoIBChJQYW5lbEZpbGxHYXBBbnN3ZXISPQoGdmFsdWVzGAEgAygLMi0ucHVibGljX2FwaS52MS5QYW5lbEZpbGxHYXBBbnN3ZXIuVmFsdWVzRW50cnkaLQoLVmFsdWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASIrChRQYW5lbFNvcnRPcmRlckFuc3dlchITCgtvcmRlcmVkX2lkcxgBIAMoCSKDAgoLUGFuZWxBbnN3ZXISEwoLcXVlc3Rpb25faWQYASABKAkSLAoDbWNxGAogASgLMh0ucHVibGljX2FwaS52MS5QYW5lbE1jcUFuc3dlckgAEjYKCHRvZG9saXN0GAsgASgLMiIucHVibGljX2FwaS52MS5QYW5lbFRvZG9saXN0QW5zd2VySAASNQoIZmlsbF9nYXAYDCABKAsyIS5wdWJsaWNfYXBpLnYxLlBhbmVsRmlsbEdhcEFuc3dlckgAEjkKCnNvcnRfb3JkZXIYDSABKAsyIy5wdWJsaWNfYXBpLnYxLlBhbmVsU29ydE9yZGVyQW5zd2VySABCBwoFdmFsdWUijQEKE1BhbmVsVGVzdFN1Ym1pc3Npb24SCgoCaWQYASABKAkSFQoNc2NvcmVfcGVyY2VudBgCIAEoBRIOCgZwYXNzZWQYAyABKAgSEwoLZHVyYXRpb25fbXMYBCABKAMSLgoKY3JlYXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAilwEKE1BhbmVsUXVlc3Rpb25SZXN1bHQSEwoLcXVlc3Rpb25faWQYASABKAkSDwoHY29ycmVjdBgCIAEoCBIOCgZwb2ludHMYAyABKAUSNwoOY29ycmVjdF9hbnN3ZXIYBCABKAsyGi5wdWJsaWNfYXBpLnYxLlBhbmVsQW5zd2VySACIAQFCEQoPX2NvcnJlY3RfYW5zd2VyIpgBChpVc2VyUGFuZWxTdWJtaXRUZXN0UmVxdWVzdBIUCgxhY2Nlc3NfdG9rZW4YASABKAkSEQoJYWNjZXNzX2lkGAIgASgJEg8KB3Rlc3RfaWQYAyABKAkSKwoHYW5zd2VycxgEIAMoCzIaLnB1YmxpY19hcGkudjEuUGFuZWxBbnN3ZXISEwoLZHVyYXRpb25fbXMYBSABKAMiyQIKG1VzZXJQYW5lbFN1Ym1pdFRlc3RSZXNwb25zZRJECgdzdWNjZXNzGAEgASgLMjEucHVibGljX2FwaS52MS5Vc2VyUGFuZWxTdWJtaXRUZXN0UmVzcG9uc2UuUmVzdWx0SAASNAoHZmFpbHVyZRgCIAEoDjIhLnB1YmxpY19hcGkudjEuUGFuZWxDb3Vyc2VGYWlsdXJlSAAaowEKBlJlc3VsdBI2CgpzdWJtaXNzaW9uGAEgASgLMiIucHVibGljX2FwaS52MS5QYW5lbFRlc3RTdWJtaXNzaW9uEjMKB3Jlc3VsdHMYAiADKAsyIi5wdWJsaWNfYXBpLnYxLlBhbmVsUXVlc3Rpb25SZXN1bHQSGgoNYXR0ZW1wdHNfbGVmdBgDIAEoBUgAiAEBQhAKDl9hdHRlbXB0c19sZWZ0QggKBnJlc3VsdCJfCiNVc2VyUGFuZWxMaXN0VGVzdFN1Ym1pc3Npb25zUmVxdWVzdBIUCgxhY2Nlc3NfdG9rZW4YASABKAkSEQoJYWNjZXNzX2lkGAIgASgJEg8KB3Rlc3RfaWQYAyABKAkiqwIKJFVzZXJQYW5lbExpc3RUZXN0U3VibWlzc2lvbnNSZXNwb25zZRJNCgdzdWNjZXNzGAEgASgLMjoucHVibGljX2FwaS52MS5Vc2VyUGFuZWxMaXN0VGVzdFN1Ym1pc3Npb25zUmVzcG9uc2UuUmVzdWx0SAASNAoHZmFpbHVyZRgCIAEoDjIhLnB1YmxpY19hcGkudjEuUGFuZWxDb3Vyc2VGYWlsdXJlSAAadAoGUmVzdWx0EjcKC3N1Ym1pc3Npb25zGAEgAygLMiIucHVibGljX2FwaS52MS5QYW5lbFRlc3RTdWJtaXNzaW9uEhoKEmJlc3Rfc2NvcmVfcGVyY2VudBgCIAEoBRIVCg1hdHRlbXB0X2NvdW50GAMgASgFQggKBnJlc3VsdCJWChNQYW5lbFF1ZXN0aW9uQW5zd2VyEg8KB2NvbnRlbnQYASABKAkSLgoKY3JlYXRlZF9hdBgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAizAEKE1BhbmVsTGVzc29uUXVlc3Rpb24SCgoCaWQYASABKAkSDwoHY29udGVudBgCIAEoCRITCgthdXRob3JfbmFtZRgDIAEoCRIPCgdpc19taW5lGAQgASgIEi4KCmNyZWF0ZWRfYXQYBSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjcKBmFuc3dlchgGIAEoCzIiLnB1YmxpY19hcGkudjEuUGFuZWxRdWVzdGlvbkFuc3dlckgAiAEBQgkKB19hbnN3ZXIiYQojVXNlclBhbmVsTGlzdExlc3NvblF1ZXN0aW9uc1JlcXVlc3QSFAoMYWNjZXNzX3Rva2VuGAEgASgJEhEKCWFjY2Vzc19pZBgCIAEoCRIRCglsZXNzb25faWQYAyABKAki9gEKJFVzZXJQYW5lbExpc3RMZXNzb25RdWVzdGlvbnNSZXNwb25zZRJNCgdzdWNjZXNzGAEgASgLMjoucHVibGljX2FwaS52MS5Vc2VyUGFuZWxMaXN0TGVzc29uUXVlc3Rpb25zUmVzcG9uc2UuUmVzdWx0SAASNAoHZmFpbHVyZRgCIAEoDjIhLnB1YmxpY19hcGkudjEuUGFuZWxDb3Vyc2VGYWlsdXJlSAAaPwoGUmVzdWx0EjUKCXF1ZXN0aW9ucxgBIAMoCzIiLnB1YmxpY19hcGkudjEuUGFuZWxMZXNzb25RdWVzdGlvbkIICgZyZXN1bHQicAohVXNlclBhbmVsQXNrTGVzc29uUXVlc3Rpb25SZXF1ZXN0EhQKDGFjY2Vzc190b2tlbhgBIAEoCRIRCglhY2Nlc3NfaWQYAiABKAkSEQoJbGVzc29uX2lkGAMgASgJEg8KB2NvbnRlbnQYBCABKAkimwEKIlVzZXJQYW5lbEFza0xlc3NvblF1ZXN0aW9uUmVzcG9uc2USNQoHc3VjY2VzcxgBIAEoCzIiLnB1YmxpY19hcGkudjEuUGFuZWxMZXNzb25RdWVzdGlvbkgAEjQKB2ZhaWx1cmUYAiABKA4yIS5wdWJsaWNfYXBpLnYxLlBhbmVsQ291cnNlRmFpbHVyZUgAQggKBnJlc3VsdCJ4ChpVc2VyUGFuZWxSYXRlTGVzc29uUmVxdWVzdBIUCgxhY2Nlc3NfdG9rZW4YASABKAkSEQoJYWNjZXNzX2lkGAIgASgJEhEKCWxlc3Nvbl9pZBgDIAEoCRINCgVzY29yZRgEIAEoBRIPCgdjb21tZW50GAUgASgJIu4BChtVc2VyUGFuZWxSYXRlTGVzc29uUmVzcG9uc2USRAoHc3VjY2VzcxgBIAEoCzIxLnB1YmxpY19hcGkudjEuVXNlclBhbmVsUmF0ZUxlc3NvblJlc3BvbnNlLlJlc3VsdEgAEjQKB2ZhaWx1cmUYAiABKA4yIS5wdWJsaWNfYXBpLnYxLlBhbmVsQ291cnNlRmFpbHVyZUgAGkkKBlJlc3VsdBIRCglteV9yYXRpbmcYASABKAUSFgoOYXZlcmFnZV9yYXRpbmcYAiABKAISFAoMcmF0aW5nX2NvdW50GAMgASgFQggKBnJlc3VsdCr6AgoSUGFuZWxDb3Vyc2VGYWlsdXJlEiAKHFBBTkVMX0NPVVJTRV9GQUlMVVJFX1VOS05PV04QABIlCiFQQU5FTF9DT1VSU0VfRkFJTFVSRV9VTkFVVEhPUklaRUQQARIpCiVQQU5FTF9DT1VSU0VfRkFJTFVSRV9DT1VSU0VfTk9UX0ZPVU5EEAISJwojUEFORUxfQ09VUlNFX0ZBSUxVUkVfSVRFTV9OT1RfRk9VTkQQAxIkCiBQQU5FTF9DT1VSU0VfRkFJTFVSRV9JVEVNX0xPQ0tFRBAEEikKJVBBTkVMX0NPVVJTRV9GQUlMVVJFX05PX0FUVEVNUFRTX0xFRlQQBRIpCiVQQU5FTF9DT1VSU0VfRkFJTFVSRV9JTlZBTElEX0FSR1VNRU5UEAYSJQohUEFORUxfQ09VUlNFX0ZBSUxVUkVfUkFURV9MSU1JVEVEEAcSJAogUEFORUxfQ09VUlNFX0ZBSUxVUkVfVU5BVkFJTEFCTEUQCCptCg9QYW5lbENvdXJzZVR5cGUSHQoZUEFORUxfQ09VUlNFX1RZUEVfVU5LTk9XThAAEh4KGlBBTkVMX0NPVVJTRV9UWVBFX1NUQU5EQVJEEAESGwoXUEFORUxfQ09VUlNFX1RZUEVfVklERU8QAip9ChNQYW5lbENvdXJzZUl0ZW1LaW5kEiIKHlBBTkVMX0NPVVJTRV9JVEVNX0tJTkRfVU5LTk9XThAAEiEKHVBBTkVMX0NPVVJTRV9JVEVNX0tJTkRfTEVTU09OEAESHwobUEFORUxfQ09VUlNFX0lURU1fS0lORF9URVNUEAIy+QcKFlVzZXJQYW5lbENvdXJzZVNlcnZpY2USYAoJR2V0Q291cnNlEigucHVibGljX2FwaS52MS5Vc2VyUGFuZWxHZXRDb3Vyc2VSZXF1ZXN0GikucHVibGljX2FwaS52MS5Vc2VyUGFuZWxHZXRDb3Vyc2VSZXNwb25zZRJgCglHZXRMZXNzb24SKC5wdWJsaWNfYXBpLnYxLlVzZXJQYW5lbEdldExlc3NvblJlcXVlc3QaKS5wdWJsaWNfYXBpLnYxLlVzZXJQYW5lbEdldExlc3NvblJlc3BvbnNlEnsKElNhdmVMZXNzb25Qcm9ncmVzcxIxLnB1YmxpY19hcGkudjEuVXNlclBhbmVsU2F2ZUxlc3NvblByb2dyZXNzUmVxdWVzdBoyLnB1YmxpY19hcGkudjEuVXNlclBhbmVsU2F2ZUxlc3NvblByb2dyZXNzUmVzcG9uc2USWgoHR2V0VGVzdBImLnB1YmxpY19hcGkudjEuVXNlclBhbmVsR2V0VGVzdFJlcXVlc3QaJy5wdWJsaWNfYXBpLnYxLlVzZXJQYW5lbEdldFRlc3RSZXNwb25zZRJjCgpTdWJtaXRUZXN0EikucHVibGljX2FwaS52MS5Vc2VyUGFuZWxTdWJtaXRUZXN0UmVxdWVzdBoqLnB1YmxpY19hcGkudjEuVXNlclBhbmVsU3VibWl0VGVzdFJlc3BvbnNlEn4KE0xpc3RUZXN0U3VibWlzc2lvbnMSMi5wdWJsaWNfYXBpLnYxLlVzZXJQYW5lbExpc3RUZXN0U3VibWlzc2lvbnNSZXF1ZXN0GjMucHVibGljX2FwaS52MS5Vc2VyUGFuZWxMaXN0VGVzdFN1Ym1pc3Npb25zUmVzcG9uc2USfgoTTGlzdExlc3NvblF1ZXN0aW9ucxIyLnB1YmxpY19hcGkudjEuVXNlclBhbmVsTGlzdExlc3NvblF1ZXN0aW9uc1JlcXVlc3QaMy5wdWJsaWNfYXBpLnYxLlVzZXJQYW5lbExpc3RMZXNzb25RdWVzdGlvbnNSZXNwb25zZRJ4ChFBc2tMZXNzb25RdWVzdGlvbhIwLnB1YmxpY19hcGkudjEuVXNlclBhbmVsQXNrTGVzc29uUXVlc3Rpb25SZXF1ZXN0GjEucHVibGljX2FwaS52MS5Vc2VyUGFuZWxBc2tMZXNzb25RdWVzdGlvblJlc3BvbnNlEmMKClJhdGVMZXNzb24SKS5wdWJsaWNfYXBpLnYxLlVzZXJQYW5lbFJhdGVMZXNzb25SZXF1ZXN0GioucHVibGljX2FwaS52MS5Vc2VyUGFuZWxSYXRlTGVzc29uUmVzcG9uc2ViBnByb3RvMw\", [file_google_protobuf_timestamp]);\n\n/**\n * Why an item is locked.\n *\n * @generated from message public_api.v1.PanelLockReason\n */\nexport type PanelLockReason = Message<\"public_api.v1.PanelLockReason\"> & {\n /**\n * The test (of an earlier module) that has to be passed first.\n *\n * @generated from field: string gating_test_id = 1;\n */\n gatingTestId: string;\n\n /**\n * @generated from field: string gating_test_title = 2;\n */\n gatingTestTitle: string;\n};\n\n/**\n * Describes the message public_api.v1.PanelLockReason.\n * Use `create(PanelLockReasonSchema)` to create a new message.\n */\nexport const PanelLockReasonSchema: GenMessage<PanelLockReason> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 0);\n\n/**\n * A lesson or a test in the list of a course.\n *\n * @generated from message public_api.v1.PanelCourseItem\n */\nexport type PanelCourseItem = Message<\"public_api.v1.PanelCourseItem\"> & {\n /**\n * Unique among all items of the course, lessons and tests alike.\n *\n * @generated from field: string id = 1;\n */\n id: string;\n\n /**\n * @generated from field: public_api.v1.PanelCourseItemKind kind = 2;\n */\n kind: PanelCourseItemKind;\n\n /**\n * @generated from field: string title = 3;\n */\n title: string;\n\n /**\n * Free for everyone, shown as a badge.\n *\n * @generated from field: bool is_free = 4;\n */\n isFree: boolean;\n\n /**\n * A locked item can't be opened (GetLesson/GetTest fail with ITEM_LOCKED).\n *\n * @generated from field: bool locked = 5;\n */\n locked: boolean;\n\n /**\n * Set when locked.\n *\n * @generated from field: optional public_api.v1.PanelLockReason lock_reason = 6;\n */\n lockReason?: PanelLockReason;\n\n /**\n * A lesson: completed. A test: passed.\n *\n * @generated from field: bool completed = 7;\n */\n completed: boolean;\n\n /**\n * Video length in seconds; 0 when unknown or the lesson has no video.\n *\n * @generated from field: int32 duration_seconds = 8;\n */\n durationSeconds: number;\n\n /**\n * Signed URL of the thumbnail; unset when there is none.\n *\n * @generated from field: optional string thumbnail_url = 9;\n */\n thumbnailUrl?: string;\n\n /**\n * Tests only: lessons of the following sections stay locked until it is passed.\n *\n * @generated from field: bool gating = 10;\n */\n gating: boolean;\n};\n\n/**\n * Describes the message public_api.v1.PanelCourseItem.\n * Use `create(PanelCourseItemSchema)` to create a new message.\n */\nexport const PanelCourseItemSchema: GenMessage<PanelCourseItem> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 1);\n\n/**\n * A module (standard course) or a section (video course).\n *\n * @generated from message public_api.v1.PanelCourseSection\n */\nexport type PanelCourseSection = Message<\"public_api.v1.PanelCourseSection\"> & {\n /**\n * @generated from field: string id = 1;\n */\n id: string;\n\n /**\n * @generated from field: string title = 2;\n */\n title: string;\n\n /**\n * @generated from field: string description = 3;\n */\n description: string;\n\n /**\n * Lessons in order, then the tests of the section.\n *\n * @generated from field: repeated public_api.v1.PanelCourseItem items = 4;\n */\n items: PanelCourseItem[];\n};\n\n/**\n * Describes the message public_api.v1.PanelCourseSection.\n * Use `create(PanelCourseSectionSchema)` to create a new message.\n */\nexport const PanelCourseSectionSchema: GenMessage<PanelCourseSection> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 2);\n\n/**\n * @generated from message public_api.v1.PanelCourse\n */\nexport type PanelCourse = Message<\"public_api.v1.PanelCourse\"> & {\n /**\n * Id of the course in Bitsnap (stable, used in the URL of the panel).\n *\n * @generated from field: string id = 1;\n */\n id: string;\n\n /**\n * @generated from field: public_api.v1.PanelCourseType type = 2;\n */\n type: PanelCourseType;\n\n /**\n * @generated from field: string title = 3;\n */\n title: string;\n\n /**\n * @generated from field: string description = 4;\n */\n description: string;\n\n /**\n * Signed URL of the cover; unset when the course has none.\n *\n * @generated from field: optional string cover_url = 5;\n */\n coverUrl?: string;\n\n /**\n * @generated from field: int32 total_lessons = 6;\n */\n totalLessons: number;\n\n /**\n * @generated from field: int32 completed_lessons = 7;\n */\n completedLessons: number;\n\n /**\n * 0 to 100.\n *\n * @generated from field: int32 progress_percent = 8;\n */\n progressPercent: number;\n\n /**\n * @generated from field: repeated public_api.v1.PanelCourseSection sections = 9;\n */\n sections: PanelCourseSection[];\n\n /**\n * Tests of the whole course, shown after the last section.\n *\n * @generated from field: repeated public_api.v1.PanelCourseItem final_items = 10;\n */\n finalItems: PanelCourseItem[];\n\n /**\n * Item to continue with (first unfinished and unlocked lesson); empty when\n * everything is done.\n *\n * @generated from field: string resume_item_id = 11;\n */\n resumeItemId: string;\n};\n\n/**\n * Describes the message public_api.v1.PanelCourse.\n * Use `create(PanelCourseSchema)` to create a new message.\n */\nexport const PanelCourseSchema: GenMessage<PanelCourse> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 3);\n\n/**\n * @generated from message public_api.v1.UserPanelGetCourseRequest\n */\nexport type UserPanelGetCourseRequest = Message<\"public_api.v1.UserPanelGetCourseRequest\"> & {\n /**\n * @generated from field: string access_token = 1;\n */\n accessToken: string;\n\n /**\n * @generated from field: string access_id = 2;\n */\n accessId: string;\n};\n\n/**\n * Describes the message public_api.v1.UserPanelGetCourseRequest.\n * Use `create(UserPanelGetCourseRequestSchema)` to create a new message.\n */\nexport const UserPanelGetCourseRequestSchema: GenMessage<UserPanelGetCourseRequest> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 4);\n\n/**\n * @generated from message public_api.v1.UserPanelGetCourseResponse\n */\nexport type UserPanelGetCourseResponse = Message<\"public_api.v1.UserPanelGetCourseResponse\"> & {\n /**\n * @generated from oneof public_api.v1.UserPanelGetCourseResponse.result\n */\n result: {\n /**\n * @generated from field: public_api.v1.PanelCourse success = 1;\n */\n value: PanelCourse;\n case: \"success\";\n } | {\n /**\n * @generated from field: public_api.v1.PanelCourseFailure failure = 2;\n */\n value: PanelCourseFailure;\n case: \"failure\";\n } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message public_api.v1.UserPanelGetCourseResponse.\n * Use `create(UserPanelGetCourseResponseSchema)` to create a new message.\n */\nexport const UserPanelGetCourseResponseSchema: GenMessage<UserPanelGetCourseResponse> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 5);\n\n/**\n * @generated from message public_api.v1.PanelVideo\n */\nexport type PanelVideo = Message<\"public_api.v1.PanelVideo\"> & {\n /**\n * Cloudflare Stream HLS manifest URL with a signed token.\n *\n * @generated from field: string hls_url = 1;\n */\n hlsUrl: string;\n\n /**\n * @generated from field: optional string poster_url = 2;\n */\n posterUrl?: string;\n\n /**\n * @generated from field: int32 duration_seconds = 3;\n */\n durationSeconds: number;\n};\n\n/**\n * Describes the message public_api.v1.PanelVideo.\n * Use `create(PanelVideoSchema)` to create a new message.\n */\nexport const PanelVideoSchema: GenMessage<PanelVideo> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 6);\n\n/**\n * @generated from message public_api.v1.PanelAttachment\n */\nexport type PanelAttachment = Message<\"public_api.v1.PanelAttachment\"> & {\n /**\n * @generated from field: string id = 1;\n */\n id: string;\n\n /**\n * @generated from field: string name = 2;\n */\n name: string;\n\n /**\n * Bytes.\n *\n * @generated from field: int64 size = 3;\n */\n size: bigint;\n\n /**\n * Presigned S3 URL.\n *\n * @generated from field: string url = 4;\n */\n url: string;\n};\n\n/**\n * Describes the message public_api.v1.PanelAttachment.\n * Use `create(PanelAttachmentSchema)` to create a new message.\n */\nexport const PanelAttachmentSchema: GenMessage<PanelAttachment> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 7);\n\n/**\n * Neighbour of a lesson in the course order.\n *\n * @generated from message public_api.v1.PanelItemRef\n */\nexport type PanelItemRef = Message<\"public_api.v1.PanelItemRef\"> & {\n /**\n * @generated from field: string id = 1;\n */\n id: string;\n\n /**\n * @generated from field: public_api.v1.PanelCourseItemKind kind = 2;\n */\n kind: PanelCourseItemKind;\n};\n\n/**\n * Describes the message public_api.v1.PanelItemRef.\n * Use `create(PanelItemRefSchema)` to create a new message.\n */\nexport const PanelItemRefSchema: GenMessage<PanelItemRef> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 8);\n\n/**\n * @generated from message public_api.v1.PanelLesson\n */\nexport type PanelLesson = Message<\"public_api.v1.PanelLesson\"> & {\n /**\n * @generated from field: string id = 1;\n */\n id: string;\n\n /**\n * @generated from field: string section_id = 2;\n */\n sectionId: string;\n\n /**\n * @generated from field: string title = 3;\n */\n title: string;\n\n /**\n * Plain text, mainly for video lessons.\n *\n * @generated from field: string description = 4;\n */\n description: string;\n\n /**\n * HTML from the editor of the creator. The client sanitises it before it\n * renders it, the server should too.\n *\n * @generated from field: string content_html = 5;\n */\n contentHtml: string;\n\n /**\n * Unset when the lesson has no video.\n *\n * @generated from field: optional public_api.v1.PanelVideo video = 6;\n */\n video?: PanelVideo;\n\n /**\n * @generated from field: repeated public_api.v1.PanelAttachment attachments = 7;\n */\n attachments: PanelAttachment[];\n\n /**\n * Unset for the first / last item of the course.\n *\n * @generated from field: optional public_api.v1.PanelItemRef prev = 8;\n */\n prev?: PanelItemRef;\n\n /**\n * @generated from field: optional public_api.v1.PanelItemRef next = 9;\n */\n next?: PanelItemRef;\n\n /**\n * @generated from field: bool completed = 10;\n */\n completed: boolean;\n\n /**\n * Where the customer stopped the video, to resume from.\n *\n * @generated from field: int32 position_seconds = 11;\n */\n positionSeconds: number;\n\n /**\n * Rating of the customer, 0 when not rated.\n *\n * @generated from field: int32 my_rating = 12;\n */\n myRating: number;\n\n /**\n * @generated from field: float average_rating = 13;\n */\n averageRating: number;\n\n /**\n * @generated from field: int32 rating_count = 14;\n */\n ratingCount: number;\n\n /**\n * When the signed URLs (video, attachments) stop working; unset when unknown.\n *\n * @generated from field: optional google.protobuf.Timestamp urls_expire_at = 15;\n */\n urlsExpireAt?: Timestamp;\n};\n\n/**\n * Describes the message public_api.v1.PanelLesson.\n * Use `create(PanelLessonSchema)` to create a new message.\n */\nexport const PanelLessonSchema: GenMessage<PanelLesson> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 9);\n\n/**\n * @generated from message public_api.v1.UserPanelGetLessonRequest\n */\nexport type UserPanelGetLessonRequest = Message<\"public_api.v1.UserPanelGetLessonRequest\"> & {\n /**\n * @generated from field: string access_token = 1;\n */\n accessToken: string;\n\n /**\n * @generated from field: string access_id = 2;\n */\n accessId: string;\n\n /**\n * @generated from field: string lesson_id = 3;\n */\n lessonId: string;\n};\n\n/**\n * Describes the message public_api.v1.UserPanelGetLessonRequest.\n * Use `create(UserPanelGetLessonRequestSchema)` to create a new message.\n */\nexport const UserPanelGetLessonRequestSchema: GenMessage<UserPanelGetLessonRequest> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 10);\n\n/**\n * @generated from message public_api.v1.UserPanelGetLessonResponse\n */\nexport type UserPanelGetLessonResponse = Message<\"public_api.v1.UserPanelGetLessonResponse\"> & {\n /**\n * @generated from oneof public_api.v1.UserPanelGetLessonResponse.result\n */\n result: {\n /**\n * @generated from field: public_api.v1.PanelLesson success = 1;\n */\n value: PanelLesson;\n case: \"success\";\n } | {\n /**\n * @generated from field: public_api.v1.PanelCourseFailure failure = 2;\n */\n value: PanelCourseFailure;\n case: \"failure\";\n } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message public_api.v1.UserPanelGetLessonResponse.\n * Use `create(UserPanelGetLessonResponseSchema)` to create a new message.\n */\nexport const UserPanelGetLessonResponseSchema: GenMessage<UserPanelGetLessonResponse> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 11);\n\n/**\n * @generated from message public_api.v1.UserPanelSaveLessonProgressRequest\n */\nexport type UserPanelSaveLessonProgressRequest = Message<\"public_api.v1.UserPanelSaveLessonProgressRequest\"> & {\n /**\n * @generated from field: string access_token = 1;\n */\n accessToken: string;\n\n /**\n * @generated from field: string access_id = 2;\n */\n accessId: string;\n\n /**\n * @generated from field: string lesson_id = 3;\n */\n lessonId: string;\n\n /**\n * Mark the lesson completed. Never un-completes a lesson.\n *\n * @generated from field: bool completed = 4;\n */\n completed: boolean;\n\n /**\n * Playback position in seconds.\n *\n * @generated from field: int32 position_seconds = 5;\n */\n positionSeconds: number;\n\n /**\n * Length of the video in seconds, so the server can compute the percentage.\n *\n * @generated from field: int32 duration_seconds = 6;\n */\n durationSeconds: number;\n};\n\n/**\n * Describes the message public_api.v1.UserPanelSaveLessonProgressRequest.\n * Use `create(UserPanelSaveLessonProgressRequestSchema)` to create a new message.\n */\nexport const UserPanelSaveLessonProgressRequestSchema: GenMessage<UserPanelSaveLessonProgressRequest> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 12);\n\n/**\n * @generated from message public_api.v1.UserPanelSaveLessonProgressResponse\n */\nexport type UserPanelSaveLessonProgressResponse = Message<\"public_api.v1.UserPanelSaveLessonProgressResponse\"> & {\n /**\n * @generated from oneof public_api.v1.UserPanelSaveLessonProgressResponse.result\n */\n result: {\n /**\n * @generated from field: public_api.v1.UserPanelSaveLessonProgressResponse.Result success = 1;\n */\n value: UserPanelSaveLessonProgressResponse_Result;\n case: \"success\";\n } | {\n /**\n * @generated from field: public_api.v1.PanelCourseFailure failure = 2;\n */\n value: PanelCourseFailure;\n case: \"failure\";\n } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message public_api.v1.UserPanelSaveLessonProgressResponse.\n * Use `create(UserPanelSaveLessonProgressResponseSchema)` to create a new message.\n */\nexport const UserPanelSaveLessonProgressResponseSchema: GenMessage<UserPanelSaveLessonProgressResponse> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 13);\n\n/**\n * @generated from message public_api.v1.UserPanelSaveLessonProgressResponse.Result\n */\nexport type UserPanelSaveLessonProgressResponse_Result = Message<\"public_api.v1.UserPanelSaveLessonProgressResponse.Result\"> & {\n /**\n * @generated from field: bool completed = 1;\n */\n completed: boolean;\n\n /**\n * Updated numbers of the whole course.\n *\n * @generated from field: int32 completed_lessons = 2;\n */\n completedLessons: number;\n\n /**\n * @generated from field: int32 progress_percent = 3;\n */\n progressPercent: number;\n};\n\n/**\n * Describes the message public_api.v1.UserPanelSaveLessonProgressResponse.Result.\n * Use `create(UserPanelSaveLessonProgressResponse_ResultSchema)` to create a new message.\n */\nexport const UserPanelSaveLessonProgressResponse_ResultSchema: GenMessage<UserPanelSaveLessonProgressResponse_Result> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 13, 0);\n\n/**\n * @generated from message public_api.v1.PanelChoice\n */\nexport type PanelChoice = Message<\"public_api.v1.PanelChoice\"> & {\n /**\n * @generated from field: string id = 1;\n */\n id: string;\n\n /**\n * @generated from field: string label = 2;\n */\n label: string;\n};\n\n/**\n * Describes the message public_api.v1.PanelChoice.\n * Use `create(PanelChoiceSchema)` to create a new message.\n */\nexport const PanelChoiceSchema: GenMessage<PanelChoice> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 14);\n\n/**\n * Multiple choice question.\n *\n * @generated from message public_api.v1.PanelMcqConfig\n */\nexport type PanelMcqConfig = Message<\"public_api.v1.PanelMcqConfig\"> & {\n /**\n * @generated from field: repeated public_api.v1.PanelChoice choices = 1;\n */\n choices: PanelChoice[];\n\n /**\n * True when more than one choice may be selected (checkboxes), false for\n * radio buttons. Does not tell how many answers are correct.\n *\n * @generated from field: bool multiple = 2;\n */\n multiple: boolean;\n};\n\n/**\n * Describes the message public_api.v1.PanelMcqConfig.\n * Use `create(PanelMcqConfigSchema)` to create a new message.\n */\nexport const PanelMcqConfigSchema: GenMessage<PanelMcqConfig> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 15);\n\n/**\n * A checklist: all required steps have to be checked.\n *\n * @generated from message public_api.v1.PanelTodolistConfig\n */\nexport type PanelTodolistConfig = Message<\"public_api.v1.PanelTodolistConfig\"> & {\n /**\n * @generated from field: repeated public_api.v1.PanelTodolistConfig.Step steps = 1;\n */\n steps: PanelTodolistConfig_Step[];\n};\n\n/**\n * Describes the message public_api.v1.PanelTodolistConfig.\n * Use `create(PanelTodolistConfigSchema)` to create a new message.\n */\nexport const PanelTodolistConfigSchema: GenMessage<PanelTodolistConfig> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 16);\n\n/**\n * @generated from message public_api.v1.PanelTodolistConfig.Step\n */\nexport type PanelTodolistConfig_Step = Message<\"public_api.v1.PanelTodolistConfig.Step\"> & {\n /**\n * @generated from field: string id = 1;\n */\n id: string;\n\n /**\n * @generated from field: string label = 2;\n */\n label: string;\n\n /**\n * @generated from field: bool required = 3;\n */\n required: boolean;\n};\n\n/**\n * Describes the message public_api.v1.PanelTodolistConfig.Step.\n * Use `create(PanelTodolistConfig_StepSchema)` to create a new message.\n */\nexport const PanelTodolistConfig_StepSchema: GenMessage<PanelTodolistConfig_Step> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 16, 0);\n\n/**\n * Text with gaps.\n *\n * @generated from message public_api.v1.PanelFillGapConfig\n */\nexport type PanelFillGapConfig = Message<\"public_api.v1.PanelFillGapConfig\"> & {\n /**\n * Text with placeholders `{{blank_id}}`, one for each id in `blanks`.\n *\n * @generated from field: string template = 1;\n */\n template: string;\n\n /**\n * @generated from field: repeated string blanks = 2;\n */\n blanks: string[];\n};\n\n/**\n * Describes the message public_api.v1.PanelFillGapConfig.\n * Use `create(PanelFillGapConfigSchema)` to create a new message.\n */\nexport const PanelFillGapConfigSchema: GenMessage<PanelFillGapConfig> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 17);\n\n/**\n * Order the items. The server returns them already shuffled.\n *\n * @generated from message public_api.v1.PanelSortOrderConfig\n */\nexport type PanelSortOrderConfig = Message<\"public_api.v1.PanelSortOrderConfig\"> & {\n /**\n * @generated from field: repeated public_api.v1.PanelChoice items = 1;\n */\n items: PanelChoice[];\n};\n\n/**\n * Describes the message public_api.v1.PanelSortOrderConfig.\n * Use `create(PanelSortOrderConfigSchema)` to create a new message.\n */\nexport const PanelSortOrderConfigSchema: GenMessage<PanelSortOrderConfig> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 18);\n\n/**\n * @generated from message public_api.v1.PanelTestQuestion\n */\nexport type PanelTestQuestion = Message<\"public_api.v1.PanelTestQuestion\"> & {\n /**\n * @generated from field: string id = 1;\n */\n id: string;\n\n /**\n * @generated from field: string prompt = 2;\n */\n prompt: string;\n\n /**\n * @generated from field: int32 points = 3;\n */\n points: number;\n\n /**\n * @generated from oneof public_api.v1.PanelTestQuestion.config\n */\n config: {\n /**\n * @generated from field: public_api.v1.PanelMcqConfig mcq = 10;\n */\n value: PanelMcqConfig;\n case: \"mcq\";\n } | {\n /**\n * @generated from field: public_api.v1.PanelTodolistConfig todolist = 11;\n */\n value: PanelTodolistConfig;\n case: \"todolist\";\n } | {\n /**\n * @generated from field: public_api.v1.PanelFillGapConfig fill_gap = 12;\n */\n value: PanelFillGapConfig;\n case: \"fillGap\";\n } | {\n /**\n * @generated from field: public_api.v1.PanelSortOrderConfig sort_order = 13;\n */\n value: PanelSortOrderConfig;\n case: \"sortOrder\";\n } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message public_api.v1.PanelTestQuestion.\n * Use `create(PanelTestQuestionSchema)` to create a new message.\n */\nexport const PanelTestQuestionSchema: GenMessage<PanelTestQuestion> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 19);\n\n/**\n * @generated from message public_api.v1.PanelTest\n */\nexport type PanelTest = Message<\"public_api.v1.PanelTest\"> & {\n /**\n * @generated from field: string id = 1;\n */\n id: string;\n\n /**\n * @generated from field: string title = 2;\n */\n title: string;\n\n /**\n * @generated from field: string description = 3;\n */\n description: string;\n\n /**\n * Passing it unlocks the following sections.\n *\n * @generated from field: bool gating = 4;\n */\n gating: boolean;\n\n /**\n * Minimum score for a pass, 0 to 100.\n *\n * @generated from field: int32 pass_threshold_percent = 5;\n */\n passThresholdPercent: number;\n\n /**\n * 0 means unlimited.\n *\n * @generated from field: int32 attempts_allowed = 6;\n */\n attemptsAllowed: number;\n\n /**\n * Unset when the number of attempts is unlimited.\n *\n * @generated from field: optional int32 attempts_left = 7;\n */\n attemptsLeft?: number;\n\n /**\n * @generated from field: int32 attempt_count = 8;\n */\n attemptCount: number;\n\n /**\n * @generated from field: int32 best_score_percent = 9;\n */\n bestScorePercent: number;\n\n /**\n * @generated from field: bool passed = 10;\n */\n passed: boolean;\n\n /**\n * Questions in order, without the correct answers.\n *\n * @generated from field: repeated public_api.v1.PanelTestQuestion questions = 11;\n */\n questions: PanelTestQuestion[];\n};\n\n/**\n * Describes the message public_api.v1.PanelTest.\n * Use `create(PanelTestSchema)` to create a new message.\n */\nexport const PanelTestSchema: GenMessage<PanelTest> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 20);\n\n/**\n * @generated from message public_api.v1.UserPanelGetTestRequest\n */\nexport type UserPanelGetTestRequest = Message<\"public_api.v1.UserPanelGetTestRequest\"> & {\n /**\n * @generated from field: string access_token = 1;\n */\n accessToken: string;\n\n /**\n * @generated from field: string access_id = 2;\n */\n accessId: string;\n\n /**\n * @generated from field: string test_id = 3;\n */\n testId: string;\n};\n\n/**\n * Describes the message public_api.v1.UserPanelGetTestRequest.\n * Use `create(UserPanelGetTestRequestSchema)` to create a new message.\n */\nexport const UserPanelGetTestRequestSchema: GenMessage<UserPanelGetTestRequest> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 21);\n\n/**\n * @generated from message public_api.v1.UserPanelGetTestResponse\n */\nexport type UserPanelGetTestResponse = Message<\"public_api.v1.UserPanelGetTestResponse\"> & {\n /**\n * @generated from oneof public_api.v1.UserPanelGetTestResponse.result\n */\n result: {\n /**\n * @generated from field: public_api.v1.PanelTest success = 1;\n */\n value: PanelTest;\n case: \"success\";\n } | {\n /**\n * @generated from field: public_api.v1.PanelCourseFailure failure = 2;\n */\n value: PanelCourseFailure;\n case: \"failure\";\n } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message public_api.v1.UserPanelGetTestResponse.\n * Use `create(UserPanelGetTestResponseSchema)` to create a new message.\n */\nexport const UserPanelGetTestResponseSchema: GenMessage<UserPanelGetTestResponse> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 22);\n\n/**\n * @generated from message public_api.v1.PanelMcqAnswer\n */\nexport type PanelMcqAnswer = Message<\"public_api.v1.PanelMcqAnswer\"> & {\n /**\n * @generated from field: repeated string selected_ids = 1;\n */\n selectedIds: string[];\n};\n\n/**\n * Describes the message public_api.v1.PanelMcqAnswer.\n * Use `create(PanelMcqAnswerSchema)` to create a new message.\n */\nexport const PanelMcqAnswerSchema: GenMessage<PanelMcqAnswer> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 23);\n\n/**\n * @generated from message public_api.v1.PanelTodolistAnswer\n */\nexport type PanelTodolistAnswer = Message<\"public_api.v1.PanelTodolistAnswer\"> & {\n /**\n * @generated from field: repeated string checked_ids = 1;\n */\n checkedIds: string[];\n};\n\n/**\n * Describes the message public_api.v1.PanelTodolistAnswer.\n * Use `create(PanelTodolistAnswerSchema)` to create a new message.\n */\nexport const PanelTodolistAnswerSchema: GenMessage<PanelTodolistAnswer> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 24);\n\n/**\n * @generated from message public_api.v1.PanelFillGapAnswer\n */\nexport type PanelFillGapAnswer = Message<\"public_api.v1.PanelFillGapAnswer\"> & {\n /**\n * blank id -> text typed by the customer.\n *\n * @generated from field: map<string, string> values = 1;\n */\n values: { [key: string]: string };\n};\n\n/**\n * Describes the message public_api.v1.PanelFillGapAnswer.\n * Use `create(PanelFillGapAnswerSchema)` to create a new message.\n */\nexport const PanelFillGapAnswerSchema: GenMessage<PanelFillGapAnswer> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 25);\n\n/**\n * @generated from message public_api.v1.PanelSortOrderAnswer\n */\nexport type PanelSortOrderAnswer = Message<\"public_api.v1.PanelSortOrderAnswer\"> & {\n /**\n * @generated from field: repeated string ordered_ids = 1;\n */\n orderedIds: string[];\n};\n\n/**\n * Describes the message public_api.v1.PanelSortOrderAnswer.\n * Use `create(PanelSortOrderAnswerSchema)` to create a new message.\n */\nexport const PanelSortOrderAnswerSchema: GenMessage<PanelSortOrderAnswer> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 26);\n\n/**\n * An answer to one question. The variant has to match the question's config.\n *\n * @generated from message public_api.v1.PanelAnswer\n */\nexport type PanelAnswer = Message<\"public_api.v1.PanelAnswer\"> & {\n /**\n * @generated from field: string question_id = 1;\n */\n questionId: string;\n\n /**\n * @generated from oneof public_api.v1.PanelAnswer.value\n */\n value: {\n /**\n * @generated from field: public_api.v1.PanelMcqAnswer mcq = 10;\n */\n value: PanelMcqAnswer;\n case: \"mcq\";\n } | {\n /**\n * @generated from field: public_api.v1.PanelTodolistAnswer todolist = 11;\n */\n value: PanelTodolistAnswer;\n case: \"todolist\";\n } | {\n /**\n * @generated from field: public_api.v1.PanelFillGapAnswer fill_gap = 12;\n */\n value: PanelFillGapAnswer;\n case: \"fillGap\";\n } | {\n /**\n * @generated from field: public_api.v1.PanelSortOrderAnswer sort_order = 13;\n */\n value: PanelSortOrderAnswer;\n case: \"sortOrder\";\n } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message public_api.v1.PanelAnswer.\n * Use `create(PanelAnswerSchema)` to create a new message.\n */\nexport const PanelAnswerSchema: GenMessage<PanelAnswer> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 27);\n\n/**\n * @generated from message public_api.v1.PanelTestSubmission\n */\nexport type PanelTestSubmission = Message<\"public_api.v1.PanelTestSubmission\"> & {\n /**\n * @generated from field: string id = 1;\n */\n id: string;\n\n /**\n * @generated from field: int32 score_percent = 2;\n */\n scorePercent: number;\n\n /**\n * @generated from field: bool passed = 3;\n */\n passed: boolean;\n\n /**\n * @generated from field: int64 duration_ms = 4;\n */\n durationMs: bigint;\n\n /**\n * @generated from field: google.protobuf.Timestamp created_at = 5;\n */\n createdAt?: Timestamp;\n};\n\n/**\n * Describes the message public_api.v1.PanelTestSubmission.\n * Use `create(PanelTestSubmissionSchema)` to create a new message.\n */\nexport const PanelTestSubmissionSchema: GenMessage<PanelTestSubmission> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 28);\n\n/**\n * @generated from message public_api.v1.PanelQuestionResult\n */\nexport type PanelQuestionResult = Message<\"public_api.v1.PanelQuestionResult\"> & {\n /**\n * @generated from field: string question_id = 1;\n */\n questionId: string;\n\n /**\n * @generated from field: bool correct = 2;\n */\n correct: boolean;\n\n /**\n * @generated from field: int32 points = 3;\n */\n points: number;\n\n /**\n * The correct answer; only set when the \"show answers\" rule of the test\n * allows it (after submit / after pass / never).\n *\n * @generated from field: optional public_api.v1.PanelAnswer correct_answer = 4;\n */\n correctAnswer?: PanelAnswer;\n};\n\n/**\n * Describes the message public_api.v1.PanelQuestionResult.\n * Use `create(PanelQuestionResultSchema)` to create a new message.\n */\nexport const PanelQuestionResultSchema: GenMessage<PanelQuestionResult> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 29);\n\n/**\n * @generated from message public_api.v1.UserPanelSubmitTestRequest\n */\nexport type UserPanelSubmitTestRequest = Message<\"public_api.v1.UserPanelSubmitTestRequest\"> & {\n /**\n * @generated from field: string access_token = 1;\n */\n accessToken: string;\n\n /**\n * @generated from field: string access_id = 2;\n */\n accessId: string;\n\n /**\n * @generated from field: string test_id = 3;\n */\n testId: string;\n\n /**\n * Unanswered questions may be omitted; they score zero.\n *\n * @generated from field: repeated public_api.v1.PanelAnswer answers = 4;\n */\n answers: PanelAnswer[];\n\n /**\n * Time spent on the test.\n *\n * @generated from field: int64 duration_ms = 5;\n */\n durationMs: bigint;\n};\n\n/**\n * Describes the message public_api.v1.UserPanelSubmitTestRequest.\n * Use `create(UserPanelSubmitTestRequestSchema)` to create a new message.\n */\nexport const UserPanelSubmitTestRequestSchema: GenMessage<UserPanelSubmitTestRequest> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 30);\n\n/**\n * @generated from message public_api.v1.UserPanelSubmitTestResponse\n */\nexport type UserPanelSubmitTestResponse = Message<\"public_api.v1.UserPanelSubmitTestResponse\"> & {\n /**\n * @generated from oneof public_api.v1.UserPanelSubmitTestResponse.result\n */\n result: {\n /**\n * @generated from field: public_api.v1.UserPanelSubmitTestResponse.Result success = 1;\n */\n value: UserPanelSubmitTestResponse_Result;\n case: \"success\";\n } | {\n /**\n * @generated from field: public_api.v1.PanelCourseFailure failure = 2;\n */\n value: PanelCourseFailure;\n case: \"failure\";\n } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message public_api.v1.UserPanelSubmitTestResponse.\n * Use `create(UserPanelSubmitTestResponseSchema)` to create a new message.\n */\nexport const UserPanelSubmitTestResponseSchema: GenMessage<UserPanelSubmitTestResponse> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 31);\n\n/**\n * @generated from message public_api.v1.UserPanelSubmitTestResponse.Result\n */\nexport type UserPanelSubmitTestResponse_Result = Message<\"public_api.v1.UserPanelSubmitTestResponse.Result\"> & {\n /**\n * @generated from field: public_api.v1.PanelTestSubmission submission = 1;\n */\n submission?: PanelTestSubmission;\n\n /**\n * @generated from field: repeated public_api.v1.PanelQuestionResult results = 2;\n */\n results: PanelQuestionResult[];\n\n /**\n * Unset when the number of attempts is unlimited.\n *\n * @generated from field: optional int32 attempts_left = 3;\n */\n attemptsLeft?: number;\n};\n\n/**\n * Describes the message public_api.v1.UserPanelSubmitTestResponse.Result.\n * Use `create(UserPanelSubmitTestResponse_ResultSchema)` to create a new message.\n */\nexport const UserPanelSubmitTestResponse_ResultSchema: GenMessage<UserPanelSubmitTestResponse_Result> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 31, 0);\n\n/**\n * @generated from message public_api.v1.UserPanelListTestSubmissionsRequest\n */\nexport type UserPanelListTestSubmissionsRequest = Message<\"public_api.v1.UserPanelListTestSubmissionsRequest\"> & {\n /**\n * @generated from field: string access_token = 1;\n */\n accessToken: string;\n\n /**\n * @generated from field: string access_id = 2;\n */\n accessId: string;\n\n /**\n * @generated from field: string test_id = 3;\n */\n testId: string;\n};\n\n/**\n * Describes the message public_api.v1.UserPanelListTestSubmissionsRequest.\n * Use `create(UserPanelListTestSubmissionsRequestSchema)` to create a new message.\n */\nexport const UserPanelListTestSubmissionsRequestSchema: GenMessage<UserPanelListTestSubmissionsRequest> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 32);\n\n/**\n * @generated from message public_api.v1.UserPanelListTestSubmissionsResponse\n */\nexport type UserPanelListTestSubmissionsResponse = Message<\"public_api.v1.UserPanelListTestSubmissionsResponse\"> & {\n /**\n * @generated from oneof public_api.v1.UserPanelListTestSubmissionsResponse.result\n */\n result: {\n /**\n * @generated from field: public_api.v1.UserPanelListTestSubmissionsResponse.Result success = 1;\n */\n value: UserPanelListTestSubmissionsResponse_Result;\n case: \"success\";\n } | {\n /**\n * @generated from field: public_api.v1.PanelCourseFailure failure = 2;\n */\n value: PanelCourseFailure;\n case: \"failure\";\n } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message public_api.v1.UserPanelListTestSubmissionsResponse.\n * Use `create(UserPanelListTestSubmissionsResponseSchema)` to create a new message.\n */\nexport const UserPanelListTestSubmissionsResponseSchema: GenMessage<UserPanelListTestSubmissionsResponse> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 33);\n\n/**\n * @generated from message public_api.v1.UserPanelListTestSubmissionsResponse.Result\n */\nexport type UserPanelListTestSubmissionsResponse_Result = Message<\"public_api.v1.UserPanelListTestSubmissionsResponse.Result\"> & {\n /**\n * Newest first.\n *\n * @generated from field: repeated public_api.v1.PanelTestSubmission submissions = 1;\n */\n submissions: PanelTestSubmission[];\n\n /**\n * @generated from field: int32 best_score_percent = 2;\n */\n bestScorePercent: number;\n\n /**\n * @generated from field: int32 attempt_count = 3;\n */\n attemptCount: number;\n};\n\n/**\n * Describes the message public_api.v1.UserPanelListTestSubmissionsResponse.Result.\n * Use `create(UserPanelListTestSubmissionsResponse_ResultSchema)` to create a new message.\n */\nexport const UserPanelListTestSubmissionsResponse_ResultSchema: GenMessage<UserPanelListTestSubmissionsResponse_Result> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 33, 0);\n\n/**\n * @generated from message public_api.v1.PanelQuestionAnswer\n */\nexport type PanelQuestionAnswer = Message<\"public_api.v1.PanelQuestionAnswer\"> & {\n /**\n * @generated from field: string content = 1;\n */\n content: string;\n\n /**\n * @generated from field: google.protobuf.Timestamp created_at = 2;\n */\n createdAt?: Timestamp;\n};\n\n/**\n * Describes the message public_api.v1.PanelQuestionAnswer.\n * Use `create(PanelQuestionAnswerSchema)` to create a new message.\n */\nexport const PanelQuestionAnswerSchema: GenMessage<PanelQuestionAnswer> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 34);\n\n/**\n * @generated from message public_api.v1.PanelLessonQuestion\n */\nexport type PanelLessonQuestion = Message<\"public_api.v1.PanelLessonQuestion\"> & {\n /**\n * @generated from field: string id = 1;\n */\n id: string;\n\n /**\n * @generated from field: string content = 2;\n */\n content: string;\n\n /**\n * Display name resolved by Bitsnap; empty for an anonymous customer.\n *\n * @generated from field: string author_name = 3;\n */\n authorName: string;\n\n /**\n * @generated from field: bool is_mine = 4;\n */\n isMine: boolean;\n\n /**\n * @generated from field: google.protobuf.Timestamp created_at = 5;\n */\n createdAt?: Timestamp;\n\n /**\n * Unset until the course team answers.\n *\n * @generated from field: optional public_api.v1.PanelQuestionAnswer answer = 6;\n */\n answer?: PanelQuestionAnswer;\n};\n\n/**\n * Describes the message public_api.v1.PanelLessonQuestion.\n * Use `create(PanelLessonQuestionSchema)` to create a new message.\n */\nexport const PanelLessonQuestionSchema: GenMessage<PanelLessonQuestion> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 35);\n\n/**\n * @generated from message public_api.v1.UserPanelListLessonQuestionsRequest\n */\nexport type UserPanelListLessonQuestionsRequest = Message<\"public_api.v1.UserPanelListLessonQuestionsRequest\"> & {\n /**\n * @generated from field: string access_token = 1;\n */\n accessToken: string;\n\n /**\n * @generated from field: string access_id = 2;\n */\n accessId: string;\n\n /**\n * @generated from field: string lesson_id = 3;\n */\n lessonId: string;\n};\n\n/**\n * Describes the message public_api.v1.UserPanelListLessonQuestionsRequest.\n * Use `create(UserPanelListLessonQuestionsRequestSchema)` to create a new message.\n */\nexport const UserPanelListLessonQuestionsRequestSchema: GenMessage<UserPanelListLessonQuestionsRequest> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 36);\n\n/**\n * @generated from message public_api.v1.UserPanelListLessonQuestionsResponse\n */\nexport type UserPanelListLessonQuestionsResponse = Message<\"public_api.v1.UserPanelListLessonQuestionsResponse\"> & {\n /**\n * @generated from oneof public_api.v1.UserPanelListLessonQuestionsResponse.result\n */\n result: {\n /**\n * @generated from field: public_api.v1.UserPanelListLessonQuestionsResponse.Result success = 1;\n */\n value: UserPanelListLessonQuestionsResponse_Result;\n case: \"success\";\n } | {\n /**\n * @generated from field: public_api.v1.PanelCourseFailure failure = 2;\n */\n value: PanelCourseFailure;\n case: \"failure\";\n } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message public_api.v1.UserPanelListLessonQuestionsResponse.\n * Use `create(UserPanelListLessonQuestionsResponseSchema)` to create a new message.\n */\nexport const UserPanelListLessonQuestionsResponseSchema: GenMessage<UserPanelListLessonQuestionsResponse> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 37);\n\n/**\n * @generated from message public_api.v1.UserPanelListLessonQuestionsResponse.Result\n */\nexport type UserPanelListLessonQuestionsResponse_Result = Message<\"public_api.v1.UserPanelListLessonQuestionsResponse.Result\"> & {\n /**\n * Newest first.\n *\n * @generated from field: repeated public_api.v1.PanelLessonQuestion questions = 1;\n */\n questions: PanelLessonQuestion[];\n};\n\n/**\n * Describes the message public_api.v1.UserPanelListLessonQuestionsResponse.Result.\n * Use `create(UserPanelListLessonQuestionsResponse_ResultSchema)` to create a new message.\n */\nexport const UserPanelListLessonQuestionsResponse_ResultSchema: GenMessage<UserPanelListLessonQuestionsResponse_Result> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 37, 0);\n\n/**\n * @generated from message public_api.v1.UserPanelAskLessonQuestionRequest\n */\nexport type UserPanelAskLessonQuestionRequest = Message<\"public_api.v1.UserPanelAskLessonQuestionRequest\"> & {\n /**\n * @generated from field: string access_token = 1;\n */\n accessToken: string;\n\n /**\n * @generated from field: string access_id = 2;\n */\n accessId: string;\n\n /**\n * @generated from field: string lesson_id = 3;\n */\n lessonId: string;\n\n /**\n * @generated from field: string content = 4;\n */\n content: string;\n};\n\n/**\n * Describes the message public_api.v1.UserPanelAskLessonQuestionRequest.\n * Use `create(UserPanelAskLessonQuestionRequestSchema)` to create a new message.\n */\nexport const UserPanelAskLessonQuestionRequestSchema: GenMessage<UserPanelAskLessonQuestionRequest> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 38);\n\n/**\n * @generated from message public_api.v1.UserPanelAskLessonQuestionResponse\n */\nexport type UserPanelAskLessonQuestionResponse = Message<\"public_api.v1.UserPanelAskLessonQuestionResponse\"> & {\n /**\n * @generated from oneof public_api.v1.UserPanelAskLessonQuestionResponse.result\n */\n result: {\n /**\n * @generated from field: public_api.v1.PanelLessonQuestion success = 1;\n */\n value: PanelLessonQuestion;\n case: \"success\";\n } | {\n /**\n * @generated from field: public_api.v1.PanelCourseFailure failure = 2;\n */\n value: PanelCourseFailure;\n case: \"failure\";\n } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message public_api.v1.UserPanelAskLessonQuestionResponse.\n * Use `create(UserPanelAskLessonQuestionResponseSchema)` to create a new message.\n */\nexport const UserPanelAskLessonQuestionResponseSchema: GenMessage<UserPanelAskLessonQuestionResponse> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 39);\n\n/**\n * @generated from message public_api.v1.UserPanelRateLessonRequest\n */\nexport type UserPanelRateLessonRequest = Message<\"public_api.v1.UserPanelRateLessonRequest\"> & {\n /**\n * @generated from field: string access_token = 1;\n */\n accessToken: string;\n\n /**\n * @generated from field: string access_id = 2;\n */\n accessId: string;\n\n /**\n * @generated from field: string lesson_id = 3;\n */\n lessonId: string;\n\n /**\n * 1 to 5. Rating again replaces the previous rating.\n *\n * @generated from field: int32 score = 4;\n */\n score: number;\n\n /**\n * @generated from field: string comment = 5;\n */\n comment: string;\n};\n\n/**\n * Describes the message public_api.v1.UserPanelRateLessonRequest.\n * Use `create(UserPanelRateLessonRequestSchema)` to create a new message.\n */\nexport const UserPanelRateLessonRequestSchema: GenMessage<UserPanelRateLessonRequest> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 40);\n\n/**\n * @generated from message public_api.v1.UserPanelRateLessonResponse\n */\nexport type UserPanelRateLessonResponse = Message<\"public_api.v1.UserPanelRateLessonResponse\"> & {\n /**\n * @generated from oneof public_api.v1.UserPanelRateLessonResponse.result\n */\n result: {\n /**\n * @generated from field: public_api.v1.UserPanelRateLessonResponse.Result success = 1;\n */\n value: UserPanelRateLessonResponse_Result;\n case: \"success\";\n } | {\n /**\n * @generated from field: public_api.v1.PanelCourseFailure failure = 2;\n */\n value: PanelCourseFailure;\n case: \"failure\";\n } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message public_api.v1.UserPanelRateLessonResponse.\n * Use `create(UserPanelRateLessonResponseSchema)` to create a new message.\n */\nexport const UserPanelRateLessonResponseSchema: GenMessage<UserPanelRateLessonResponse> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 41);\n\n/**\n * @generated from message public_api.v1.UserPanelRateLessonResponse.Result\n */\nexport type UserPanelRateLessonResponse_Result = Message<\"public_api.v1.UserPanelRateLessonResponse.Result\"> & {\n /**\n * @generated from field: int32 my_rating = 1;\n */\n myRating: number;\n\n /**\n * @generated from field: float average_rating = 2;\n */\n averageRating: number;\n\n /**\n * @generated from field: int32 rating_count = 3;\n */\n ratingCount: number;\n};\n\n/**\n * Describes the message public_api.v1.UserPanelRateLessonResponse.Result.\n * Use `create(UserPanelRateLessonResponse_ResultSchema)` to create a new message.\n */\nexport const UserPanelRateLessonResponse_ResultSchema: GenMessage<UserPanelRateLessonResponse_Result> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 41, 0);\n\n/**\n * @generated from enum public_api.v1.PanelCourseFailure\n */\nexport enum PanelCourseFailure {\n /**\n * @generated from enum value: PANEL_COURSE_FAILURE_UNKNOWN = 0;\n */\n UNKNOWN = 0,\n\n /**\n * Missing, expired or foreign access token, or no paid access to access_id.\n *\n * @generated from enum value: PANEL_COURSE_FAILURE_UNAUTHORIZED = 1;\n */\n UNAUTHORIZED = 1,\n\n /**\n * @generated from enum value: PANEL_COURSE_FAILURE_COURSE_NOT_FOUND = 2;\n */\n COURSE_NOT_FOUND = 2,\n\n /**\n * Unknown lesson or test (or one of another course).\n *\n * @generated from enum value: PANEL_COURSE_FAILURE_ITEM_NOT_FOUND = 3;\n */\n ITEM_NOT_FOUND = 3,\n\n /**\n * The item is locked by a gating test (see PanelCourseItem.lock_reason).\n *\n * @generated from enum value: PANEL_COURSE_FAILURE_ITEM_LOCKED = 4;\n */\n ITEM_LOCKED = 4,\n\n /**\n * SubmitTest after the last allowed attempt.\n *\n * @generated from enum value: PANEL_COURSE_FAILURE_NO_ATTEMPTS_LEFT = 5;\n */\n NO_ATTEMPTS_LEFT = 5,\n\n /**\n * Malformed request, e.g. an answer to an unknown question, a rating out of\n * 1..5 or an empty question.\n *\n * @generated from enum value: PANEL_COURSE_FAILURE_INVALID_ARGUMENT = 6;\n */\n INVALID_ARGUMENT = 6,\n\n /**\n * @generated from enum value: PANEL_COURSE_FAILURE_RATE_LIMITED = 7;\n */\n RATE_LIMITED = 7,\n\n /**\n * LMS is not reachable. The client may retry.\n *\n * @generated from enum value: PANEL_COURSE_FAILURE_UNAVAILABLE = 8;\n */\n UNAVAILABLE = 8,\n}\n\n/**\n * Describes the enum public_api.v1.PanelCourseFailure.\n */\nexport const PanelCourseFailureSchema: GenEnum<PanelCourseFailure> = /*@__PURE__*/\n enumDesc(file_public_v1_user_panel_course, 0);\n\n/**\n * @generated from enum public_api.v1.PanelCourseType\n */\nexport enum PanelCourseType {\n /**\n * @generated from enum value: PANEL_COURSE_TYPE_UNKNOWN = 0;\n */\n UNKNOWN = 0,\n\n /**\n * Modules of lessons with HTML content, an optional video, attachments and\n * tests.\n *\n * @generated from enum value: PANEL_COURSE_TYPE_STANDARD = 1;\n */\n STANDARD = 1,\n\n /**\n * Sections of videos: a lesson is a video with a description, a thumbnail and\n * attachments.\n *\n * @generated from enum value: PANEL_COURSE_TYPE_VIDEO = 2;\n */\n VIDEO = 2,\n}\n\n/**\n * Describes the enum public_api.v1.PanelCourseType.\n */\nexport const PanelCourseTypeSchema: GenEnum<PanelCourseType> = /*@__PURE__*/\n enumDesc(file_public_v1_user_panel_course, 1);\n\n/**\n * @generated from enum public_api.v1.PanelCourseItemKind\n */\nexport enum PanelCourseItemKind {\n /**\n * @generated from enum value: PANEL_COURSE_ITEM_KIND_UNKNOWN = 0;\n */\n UNKNOWN = 0,\n\n /**\n * @generated from enum value: PANEL_COURSE_ITEM_KIND_LESSON = 1;\n */\n LESSON = 1,\n\n /**\n * @generated from enum value: PANEL_COURSE_ITEM_KIND_TEST = 2;\n */\n TEST = 2,\n}\n\n/**\n * Describes the enum public_api.v1.PanelCourseItemKind.\n */\nexport const PanelCourseItemKindSchema: GenEnum<PanelCourseItemKind> = /*@__PURE__*/\n enumDesc(file_public_v1_user_panel_course, 2);\n\n/**\n * Course view of the customer panel (`bitsnap-react/course`).\n *\n * DRAFT. The panel talks only to Bitsnap's /api/rpc. Bitsnap checks the paid\n * access of the customer on every call, calls the internal LMS server to\n * server and returns content with URLs that are already signed:\n * - attachments: presigned S3 URLs,\n * - video: a Cloudflare Stream HLS manifest URL with a signed token.\n * The client never sees LMS ids or LMS credentials.\n *\n * Authentication follows the other UserPanel* calls (UserPanelGetCourseDetails,\n * UserPanelGetFileDetails): `access_token` is the panel session token and\n * `access_id` is UserProduct.access_id of the purchased course. The course is\n * resolved on the server from `access_id`, so there is no project_id and no\n * course_id in the requests. All other ids are opaque strings returned by\n * GetCourse.\n *\n * Every response is `oneof result { success, failure }` like the other panel\n * calls. Gateway level errors (network, 5xx) surface as Connect errors.\n *\n * @generated from service public_api.v1.UserPanelCourseService\n */\nexport const UserPanelCourseService: GenService<{\n /**\n * Course structure with the progress of the customer.\n *\n * @generated from rpc public_api.v1.UserPanelCourseService.GetCourse\n */\n getCourse: {\n methodKind: \"unary\";\n input: typeof UserPanelGetCourseRequestSchema;\n output: typeof UserPanelGetCourseResponseSchema;\n },\n /**\n * One lesson: content, signed video and attachment URLs.\n *\n * @generated from rpc public_api.v1.UserPanelCourseService.GetLesson\n */\n getLesson: {\n methodKind: \"unary\";\n input: typeof UserPanelGetLessonRequestSchema;\n output: typeof UserPanelGetLessonResponseSchema;\n },\n /**\n * Reports the playback position and/or completion of a lesson.\n *\n * @generated from rpc public_api.v1.UserPanelCourseService.SaveLessonProgress\n */\n saveLessonProgress: {\n methodKind: \"unary\";\n input: typeof UserPanelSaveLessonProgressRequestSchema;\n output: typeof UserPanelSaveLessonProgressResponseSchema;\n },\n /**\n * A test without the correct answers.\n *\n * @generated from rpc public_api.v1.UserPanelCourseService.GetTest\n */\n getTest: {\n methodKind: \"unary\";\n input: typeof UserPanelGetTestRequestSchema;\n output: typeof UserPanelGetTestResponseSchema;\n },\n /**\n * @generated from rpc public_api.v1.UserPanelCourseService.SubmitTest\n */\n submitTest: {\n methodKind: \"unary\";\n input: typeof UserPanelSubmitTestRequestSchema;\n output: typeof UserPanelSubmitTestResponseSchema;\n },\n /**\n * @generated from rpc public_api.v1.UserPanelCourseService.ListTestSubmissions\n */\n listTestSubmissions: {\n methodKind: \"unary\";\n input: typeof UserPanelListTestSubmissionsRequestSchema;\n output: typeof UserPanelListTestSubmissionsResponseSchema;\n },\n /**\n * @generated from rpc public_api.v1.UserPanelCourseService.ListLessonQuestions\n */\n listLessonQuestions: {\n methodKind: \"unary\";\n input: typeof UserPanelListLessonQuestionsRequestSchema;\n output: typeof UserPanelListLessonQuestionsResponseSchema;\n },\n /**\n * @generated from rpc public_api.v1.UserPanelCourseService.AskLessonQuestion\n */\n askLessonQuestion: {\n methodKind: \"unary\";\n input: typeof UserPanelAskLessonQuestionRequestSchema;\n output: typeof UserPanelAskLessonQuestionResponseSchema;\n },\n /**\n * @generated from rpc public_api.v1.UserPanelCourseService.RateLesson\n */\n rateLesson: {\n methodKind: \"unary\";\n input: typeof UserPanelRateLessonRequestSchema;\n output: typeof UserPanelRateLessonResponseSchema;\n },\n}> = /*@__PURE__*/\n serviceDesc(file_public_v1_user_panel_course, 0);\n\n","import { Code, ConnectError, type Client } from \"@connectrpc/connect\";\nimport { timestampDate } from \"@bufbuild/protobuf/wkt\";\nimport {\n PanelCourseFailure,\n PanelCourseItemKind,\n PanelCourseType,\n type PanelAnswer,\n type PanelCourse,\n type PanelCourseItem,\n type PanelItemRef,\n type PanelLesson,\n type PanelLessonQuestion,\n type PanelTest,\n type PanelTestQuestion,\n type PanelTestSubmission,\n type UserPanelCourseService,\n} from \"../gen/proto/public/v1/user_panel_course_pb\";\nimport {\n CourseError,\n type Course,\n type CourseDataSource,\n type CourseErrorCode,\n type CourseItem,\n type ItemRef,\n type Lesson,\n type LessonQuestion,\n type QuestionAnswer,\n type Test,\n type TestAnswers,\n type TestQuestion,\n type TestSubmission,\n} from \"./types\";\n\n/** The part of the generated Connect client the data source calls. */\nexport type CourseRpcClient = Client<typeof UserPanelCourseService>;\n\nexport type CourseAuth = {\n /** `UserProduct.access_id` of the purchased course. */\n accessId: string;\n /** Panel session token; a function is read on every call. */\n accessToken: string | (() => string | undefined);\n};\n\nconst FAILURE_CODES: Record<PanelCourseFailure, CourseErrorCode> = {\n [PanelCourseFailure.UNKNOWN]: \"unknown\",\n [PanelCourseFailure.UNAUTHORIZED]: \"unauthorized\",\n [PanelCourseFailure.COURSE_NOT_FOUND]: \"course_not_found\",\n [PanelCourseFailure.ITEM_NOT_FOUND]: \"item_not_found\",\n [PanelCourseFailure.ITEM_LOCKED]: \"item_locked\",\n [PanelCourseFailure.NO_ATTEMPTS_LEFT]: \"no_attempts_left\",\n [PanelCourseFailure.INVALID_ARGUMENT]: \"invalid_argument\",\n [PanelCourseFailure.RATE_LIMITED]: \"rate_limited\",\n [PanelCourseFailure.UNAVAILABLE]: \"unavailable\",\n};\n\nexport function failureToCode(failure: PanelCourseFailure): CourseErrorCode {\n return FAILURE_CODES[failure] ?? \"unknown\";\n}\n\n/** Maps a transport error to a `CourseError`; other errors pass through. */\nexport function toCourseError(error: unknown): unknown {\n if (error instanceof CourseError) {\n return error;\n }\n const connectError = ConnectError.from(error);\n switch (connectError.code) {\n case Code.Unauthenticated:\n case Code.PermissionDenied:\n return new CourseError(\"unauthorized\", connectError.rawMessage);\n case Code.NotFound:\n return new CourseError(\"course_not_found\", connectError.rawMessage);\n case Code.ResourceExhausted:\n return new CourseError(\"rate_limited\", connectError.rawMessage);\n case Code.Unavailable:\n case Code.DeadlineExceeded:\n return new CourseError(\"unavailable\", connectError.rawMessage);\n default:\n return error;\n }\n}\n\ntype Result<T> = {\n result:\n | { case: \"success\"; value: T }\n | { case: \"failure\"; value: PanelCourseFailure }\n | { case: undefined; value?: undefined };\n};\n\n/** The success value of a response, or a `CourseError` for a failure. */\nexport function unwrap<T>(response: Result<T>): T {\n const { result } = response;\n if (result.case === \"success\") {\n return result.value;\n }\n if (result.case === \"failure\") {\n throw new CourseError(failureToCode(result.value));\n }\n throw new CourseError(\"unknown\", \"empty response\");\n}\n\n// Mapping from the protobuf messages\n\nfunction mapItem(item: PanelCourseItem): CourseItem {\n return {\n id: item.id,\n kind:\n item.kind === PanelCourseItemKind.TEST ? \"test\" : \"lesson\",\n title: item.title,\n isFree: item.isFree,\n locked: item.locked,\n lockReason:\n item.locked && item.lockReason != null\n ? {\n gatingTestId: item.lockReason.gatingTestId,\n gatingTestTitle: item.lockReason.gatingTestTitle,\n }\n : undefined,\n completed: item.completed,\n durationSeconds: item.durationSeconds,\n thumbnailUrl: item.thumbnailUrl || undefined,\n gating: item.gating,\n };\n}\n\nexport function mapCourse(course: PanelCourse): Course {\n return {\n id: course.id,\n type: course.type === PanelCourseType.VIDEO ? \"video\" : \"standard\",\n title: course.title,\n description: course.description,\n coverUrl: course.coverUrl || undefined,\n totalLessons: course.totalLessons,\n completedLessons: course.completedLessons,\n progressPercent: course.progressPercent,\n sections: course.sections.map((section) => ({\n id: section.id,\n title: section.title,\n description: section.description,\n items: section.items.map(mapItem),\n })),\n finalItems: course.finalItems.map(mapItem),\n resumeItemId: course.resumeItemId,\n };\n}\n\nfunction mapRef(ref: PanelItemRef | undefined): ItemRef | undefined {\n return ref == null\n ? undefined\n : {\n id: ref.id,\n kind: ref.kind === PanelCourseItemKind.TEST ? \"test\" : \"lesson\",\n };\n}\n\nexport function mapLesson(lesson: PanelLesson): Lesson {\n return {\n id: lesson.id,\n sectionId: lesson.sectionId,\n title: lesson.title,\n description: lesson.description,\n contentHtml: lesson.contentHtml,\n video:\n lesson.video != null && lesson.video.hlsUrl !== \"\"\n ? {\n hlsUrl: lesson.video.hlsUrl,\n posterUrl: lesson.video.posterUrl || undefined,\n durationSeconds: lesson.video.durationSeconds,\n }\n : undefined,\n attachments: lesson.attachments.map((a) => ({\n id: a.id,\n name: a.name,\n size: Number(a.size),\n url: a.url,\n })),\n prev: mapRef(lesson.prev),\n next: mapRef(lesson.next),\n completed: lesson.completed,\n positionSeconds: lesson.positionSeconds,\n myRating: lesson.myRating,\n averageRating: lesson.averageRating,\n ratingCount: lesson.ratingCount,\n urlsExpireAt:\n lesson.urlsExpireAt != null ? timestampDate(lesson.urlsExpireAt) : undefined,\n };\n}\n\nfunction mapQuestion(question: PanelTestQuestion): TestQuestion | undefined {\n const base = { id: question.id, prompt: question.prompt, points: question.points };\n const { config } = question;\n switch (config.case) {\n case \"mcq\":\n return {\n ...base,\n config: {\n type: \"mcq\",\n choices: config.value.choices.map((c) => ({ id: c.id, label: c.label })),\n multiple: config.value.multiple,\n },\n };\n case \"todolist\":\n return {\n ...base,\n config: {\n type: \"todolist\",\n steps: config.value.steps.map((s) => ({\n id: s.id,\n label: s.label,\n required: s.required,\n })),\n },\n };\n case \"fillGap\":\n return {\n ...base,\n config: {\n type: \"fill_gap\",\n template: config.value.template,\n blanks: [...config.value.blanks],\n },\n };\n case \"sortOrder\":\n return {\n ...base,\n config: {\n type: \"sort_order\",\n items: config.value.items.map((c) => ({ id: c.id, label: c.label })),\n },\n };\n default:\n // A kind this version of the panel does not know.\n return undefined;\n }\n}\n\nexport function mapTest(test: PanelTest): Test {\n return {\n id: test.id,\n title: test.title,\n description: test.description,\n gating: test.gating,\n passThresholdPercent: test.passThresholdPercent,\n attemptsAllowed: test.attemptsAllowed,\n attemptsLeft: test.attemptsLeft,\n attemptCount: test.attemptCount,\n bestScorePercent: test.bestScorePercent,\n passed: test.passed,\n questions: test.questions\n .map(mapQuestion)\n .filter((q): q is TestQuestion => q != null),\n };\n}\n\nexport function mapSubmission(submission: PanelTestSubmission): TestSubmission {\n return {\n id: submission.id,\n scorePercent: submission.scorePercent,\n passed: submission.passed,\n durationMs: Number(submission.durationMs),\n createdAt:\n submission.createdAt != null ? timestampDate(submission.createdAt) : undefined,\n };\n}\n\nexport function mapAnswerFromProto(answer: PanelAnswer): QuestionAnswer | undefined {\n const { value } = answer;\n switch (value.case) {\n case \"mcq\":\n return { type: \"mcq\", selectedIds: [...value.value.selectedIds] };\n case \"todolist\":\n return { type: \"todolist\", checkedIds: [...value.value.checkedIds] };\n case \"fillGap\":\n return { type: \"fill_gap\", values: { ...value.value.values } };\n case \"sortOrder\":\n return { type: \"sort_order\", orderedIds: [...value.value.orderedIds] };\n default:\n return undefined;\n }\n}\n\n/** Request shape of an answer (plain init object of `PanelAnswer`). */\nexport function mapAnswerToProto(questionId: string, answer: QuestionAnswer) {\n switch (answer.type) {\n case \"mcq\":\n return {\n questionId,\n value: { case: \"mcq\" as const, value: { selectedIds: answer.selectedIds } },\n };\n case \"todolist\":\n return {\n questionId,\n value: { case: \"todolist\" as const, value: { checkedIds: answer.checkedIds } },\n };\n case \"fill_gap\":\n return {\n questionId,\n value: { case: \"fillGap\" as const, value: { values: answer.values } },\n };\n case \"sort_order\":\n return {\n questionId,\n value: { case: \"sortOrder\" as const, value: { orderedIds: answer.orderedIds } },\n };\n }\n}\n\nfunction mapLessonQuestion(question: PanelLessonQuestion): LessonQuestion {\n return {\n id: question.id,\n content: question.content,\n authorName: question.authorName,\n isMine: question.isMine,\n createdAt:\n question.createdAt != null ? timestampDate(question.createdAt) : undefined,\n answer:\n question.answer != null\n ? {\n content: question.answer.content,\n createdAt:\n question.answer.createdAt != null\n ? timestampDate(question.answer.createdAt)\n : undefined,\n }\n : undefined,\n };\n}\n\n/**\n * Data source over `UserPanelCourseService`. Every call carries the access id\n * and the panel session token; failures become `CourseError`.\n */\nexport function createCourseDataSource(\n client: CourseRpcClient,\n auth: CourseAuth,\n): CourseDataSource {\n const credentials = () => ({\n accessId: auth.accessId,\n accessToken:\n typeof auth.accessToken === \"function\"\n ? (auth.accessToken() ?? \"\")\n : auth.accessToken,\n });\n\n async function call<T>(run: () => Promise<T>): Promise<T> {\n try {\n return await run();\n } catch (error) {\n throw toCourseError(error);\n }\n }\n\n return {\n getCourse: () =>\n call(async () => mapCourse(unwrap(await client.getCourse(credentials())))),\n\n getLesson: (lessonId) =>\n call(async () =>\n mapLesson(unwrap(await client.getLesson({ ...credentials(), lessonId }))),\n ),\n\n saveProgress: (input) =>\n call(async () => {\n const saved = unwrap(\n await client.saveLessonProgress({ ...credentials(), ...input }),\n );\n return {\n completed: saved.completed,\n completedLessons: saved.completedLessons,\n progressPercent: saved.progressPercent,\n };\n }),\n\n getTest: (testId) =>\n call(async () =>\n mapTest(unwrap(await client.getTest({ ...credentials(), testId }))),\n ),\n\n submitTest: (testId, answers: TestAnswers, durationMs) =>\n call(async () => {\n const result = unwrap(\n await client.submitTest({\n ...credentials(),\n testId,\n durationMs: BigInt(Math.max(0, Math.round(durationMs))),\n answers: Object.entries(answers).map(([questionId, answer]) =>\n mapAnswerToProto(questionId, answer),\n ),\n }),\n );\n if (result.submission == null) {\n throw new CourseError(\"unknown\", \"missing submission\");\n }\n return {\n submission: mapSubmission(result.submission),\n results: result.results.map((r) => ({\n questionId: r.questionId,\n correct: r.correct,\n points: r.points,\n correctAnswer:\n r.correctAnswer != null\n ? mapAnswerFromProto(r.correctAnswer)\n : undefined,\n })),\n attemptsLeft: result.attemptsLeft,\n };\n }),\n\n listSubmissions: (testId) =>\n call(async () => {\n const list = unwrap(\n await client.listTestSubmissions({ ...credentials(), testId }),\n );\n return {\n submissions: list.submissions.map(mapSubmission),\n bestScorePercent: list.bestScorePercent,\n attemptCount: list.attemptCount,\n };\n }),\n\n listQuestions: (lessonId) =>\n call(async () =>\n unwrap(\n await client.listLessonQuestions({ ...credentials(), lessonId }),\n ).questions.map(mapLessonQuestion),\n ),\n\n askQuestion: (lessonId, content) =>\n call(async () =>\n mapLessonQuestion(\n unwrap(\n await client.askLessonQuestion({\n ...credentials(),\n lessonId,\n content,\n }),\n ),\n ),\n ),\n\n rate: (lessonId, score, comment = \"\") =>\n call(async () => {\n const rating = unwrap(\n await client.rateLesson({ ...credentials(), lessonId, score, comment }),\n );\n return {\n myRating: rating.myRating,\n averageRating: rating.averageRating,\n ratingCount: rating.ratingCount,\n };\n }),\n };\n}\n","/**\n * Domain model of the course view. It is independent of the generated\n * protobuf messages, so a creator preview can feed `PanelCourse` through its\n * own `CourseDataSource` without the RPC layer.\n */\n\nexport type CourseType = \"standard\" | \"video\";\n\nexport type CourseItemKind = \"lesson\" | \"test\";\n\n/** The gating test that has to be passed before the item opens. */\nexport type LockReason = {\n gatingTestId: string;\n gatingTestTitle: string;\n};\n\n/** A lesson or a test in the list of a course. */\nexport type CourseItem = {\n /** Unique among all items of the course. */\n id: string;\n kind: CourseItemKind;\n title: string;\n isFree: boolean;\n locked: boolean;\n lockReason?: LockReason;\n /** A lesson: completed. A test: passed. */\n completed: boolean;\n /** Video length, 0 when unknown. */\n durationSeconds: number;\n thumbnailUrl?: string;\n /** Tests only: passing it unlocks the following sections. */\n gating: boolean;\n};\n\n/** A module (standard course) or a section (video course). */\nexport type CourseSection = {\n id: string;\n title: string;\n description: string;\n items: CourseItem[];\n};\n\nexport type Course = {\n id: string;\n type: CourseType;\n title: string;\n description: string;\n coverUrl?: string;\n totalLessons: number;\n completedLessons: number;\n /** 0 to 100. */\n progressPercent: number;\n sections: CourseSection[];\n /** Tests of the whole course, after the last section. */\n finalItems: CourseItem[];\n /** Item to continue with; empty when everything is done. */\n resumeItemId: string;\n};\n\nexport type ItemRef = {\n id: string;\n kind: CourseItemKind;\n};\n\nexport type LessonVideo = {\n hlsUrl: string;\n posterUrl?: string;\n durationSeconds: number;\n};\n\nexport type LessonAttachment = {\n id: string;\n name: string;\n /** Bytes. */\n size: number;\n url: string;\n};\n\nexport type Lesson = {\n id: string;\n sectionId: string;\n title: string;\n description: string;\n /** Raw HTML of the editor. Always pass it through `sanitizeLessonHtml`. */\n contentHtml: string;\n video?: LessonVideo;\n attachments: LessonAttachment[];\n prev?: ItemRef;\n next?: ItemRef;\n completed: boolean;\n /** Where the customer stopped the video. */\n positionSeconds: number;\n /** 0 when the customer has not rated the lesson. */\n myRating: number;\n averageRating: number;\n ratingCount: number;\n /** When the signed URLs stop working. */\n urlsExpireAt?: Date;\n};\n\nexport type SaveProgressInput = {\n lessonId: string;\n completed: boolean;\n positionSeconds: number;\n durationSeconds: number;\n};\n\nexport type ProgressSaved = {\n completed: boolean;\n completedLessons: number;\n progressPercent: number;\n};\n\n// Tests\n\nexport type TestChoice = { id: string; label: string };\n\nexport type TestQuestionConfig =\n | { type: \"mcq\"; choices: TestChoice[]; multiple: boolean }\n | {\n type: \"todolist\";\n steps: { id: string; label: string; required: boolean }[];\n }\n | { type: \"fill_gap\"; template: string; blanks: string[] }\n | { type: \"sort_order\"; items: TestChoice[] };\n\nexport type TestQuestion = {\n id: string;\n prompt: string;\n points: number;\n config: TestQuestionConfig;\n};\n\nexport type QuestionAnswer =\n | { type: \"mcq\"; selectedIds: string[] }\n | { type: \"todolist\"; checkedIds: string[] }\n | { type: \"fill_gap\"; values: Record<string, string> }\n | { type: \"sort_order\"; orderedIds: string[] };\n\n/** Answers by question id. */\nexport type TestAnswers = Record<string, QuestionAnswer>;\n\nexport type Test = {\n id: string;\n title: string;\n description: string;\n gating: boolean;\n passThresholdPercent: number;\n /** 0 means unlimited. */\n attemptsAllowed: number;\n /** `undefined` when the attempts are unlimited. */\n attemptsLeft?: number;\n attemptCount: number;\n bestScorePercent: number;\n passed: boolean;\n questions: TestQuestion[];\n};\n\nexport type TestSubmission = {\n id: string;\n scorePercent: number;\n passed: boolean;\n durationMs: number;\n createdAt?: Date;\n};\n\nexport type QuestionResult = {\n questionId: string;\n correct: boolean;\n points: number;\n /** Only when the show-answers rule of the test allows it. */\n correctAnswer?: QuestionAnswer;\n};\n\nexport type TestResult = {\n submission: TestSubmission;\n results: QuestionResult[];\n attemptsLeft?: number;\n};\n\nexport type TestSubmissionList = {\n submissions: TestSubmission[];\n bestScorePercent: number;\n attemptCount: number;\n};\n\n// Questions and ratings\n\nexport type LessonQuestion = {\n id: string;\n content: string;\n authorName: string;\n isMine: boolean;\n createdAt?: Date;\n answer?: { content: string; createdAt?: Date };\n};\n\nexport type LessonRating = {\n myRating: number;\n averageRating: number;\n ratingCount: number;\n};\n\nexport type CourseErrorCode =\n | \"unauthorized\"\n | \"course_not_found\"\n | \"item_not_found\"\n | \"item_locked\"\n | \"no_attempts_left\"\n | \"invalid_argument\"\n | \"rate_limited\"\n | \"unavailable\"\n | \"unknown\";\n\n/** Failure reported by a data source. */\nexport class CourseError extends Error {\n readonly code: CourseErrorCode;\n\n constructor(code: CourseErrorCode, message?: string) {\n super(message ?? code);\n this.name = \"CourseError\";\n this.code = code;\n }\n}\n\n/**\n * Everything `PanelCourse` needs from a backend, scoped to one course and one\n * customer. Methods reject with `CourseError` for failures the view can\n * explain (any other error is shown as a generic one).\n *\n * The default implementation is `createRpcCourseDataSource`; the creator\n * preview of the Bitsnap dashboard passes its own.\n */\nexport interface CourseDataSource {\n getCourse(): Promise<Course>;\n getLesson(lessonId: string): Promise<Lesson>;\n saveProgress(input: SaveProgressInput): Promise<ProgressSaved>;\n getTest(testId: string): Promise<Test>;\n submitTest(\n testId: string,\n answers: TestAnswers,\n durationMs: number,\n ): Promise<TestResult>;\n listSubmissions(testId: string): Promise<TestSubmissionList>;\n listQuestions(lessonId: string): Promise<LessonQuestion[]>;\n askQuestion(lessonId: string, content: string): Promise<LessonQuestion>;\n rate(\n lessonId: string,\n score: number,\n comment?: string,\n ): Promise<LessonRating>;\n}\n","import { ChevronLeft, ChevronRight, Download, Paperclip } from \"lucide-react\";\nimport { useEffect, useMemo, useRef, useState } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Button } from \"../ui/button\";\nimport { useCourseTrack } from \"./analytics\";\nimport { useCourseView } from \"./context\";\nimport { formatBytes, isPlaybackComplete, resumePosition } from \"./logic\";\nimport { LessonQuestions, LessonRatingBox } from \"./LessonSocial\";\nimport { sanitizeLessonHtml } from \"./sanitize\";\nimport type { Course, ItemRef, Lesson, ProgressSaved } from \"./types\";\nimport { ACCENT_BUTTON, CARD } from \"./ui\";\nimport { VideoPlayer, type PlaybackReport } from \"./VideoPlayer\";\n\ntype LessonPaneProps = {\n lesson: Lesson;\n /** 1-based place of the lesson among the lessons of the course. */\n position?: number;\n courseType: Course[\"type\"];\n /** Previous / next item taken from the course structure. */\n prev?: ItemRef;\n next?: ItemRef;\n autoplay: boolean;\n onAutoplayChange: (value: boolean) => void;\n /** Start playing right away (the viewer came from autoplay). */\n startPlaying: boolean;\n onNavigate: (ref: ItemRef) => void;\n onProgressSaved: (lessonId: string, saved: ProgressSaved) => void;\n onEnded: () => void;\n /** Ask for a fresh lesson (signed URLs expired). */\n onUrlsExpired: () => void;\n};\n\n/**\n * The lesson: video, text, attachments, actions, rating and questions. The\n * owner gives it a `key` of the lesson id, so a lesson never reports the\n * progress of another one.\n */\nexport function LessonPane({\n lesson,\n position = 0,\n courseType,\n prev,\n next,\n autoplay,\n onAutoplayChange,\n startPlaying,\n onNavigate,\n onProgressSaved,\n onEnded,\n onUrlsExpired,\n}: LessonPaneProps) {\n const { dataSource, texts, preview } = useCourseView();\n const track = useCourseTrack();\n const [completed, setCompleted] = useState(lesson.completed);\n const completedRef = useRef(lesson.completed);\n const lastSavedPosition = useRef<number | undefined>(undefined);\n const lastKnownPosition = useRef(lesson.positionSeconds);\n const [saving, setSaving] = useState(false);\n\n // The pane lives as long as one view of the lesson.\n useEffect(() => {\n track(\"lesson_started\", { lesson_id: lesson.id, position });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [lesson.id]);\n\n // The signed URLs of a long-open lesson expire; refresh them before then.\n const expiresAt = lesson.urlsExpireAt?.getTime();\n useEffect(() => {\n if (expiresAt == null) return;\n const delay = expiresAt - Date.now() - 60_000;\n if (delay <= 0 || delay > 2 ** 31 - 1) return;\n const timer = setTimeout(onUrlsExpired, delay);\n return () => clearTimeout(timer);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [expiresAt]);\n\n // Sanitised in the browser; the server renders no lesson HTML.\n const html = useMemo(\n () => sanitizeLessonHtml(lesson.contentHtml),\n [lesson.contentHtml],\n );\n\n async function save(\n positionSeconds: number,\n durationSeconds: number,\n markCompleted: boolean,\n ) {\n if (preview) return;\n const saved = await dataSource.saveProgress({\n lessonId: lesson.id,\n completed: markCompleted,\n positionSeconds,\n durationSeconds,\n });\n lastSavedPosition.current = positionSeconds;\n if (saved.completed && !completedRef.current) {\n completedRef.current = true;\n setCompleted(true);\n track(\"lesson_completed\", { lesson_id: lesson.id, position });\n }\n onProgressSaved(lesson.id, saved);\n }\n\n function handleReport(report: PlaybackReport) {\n lastKnownPosition.current = report.positionSeconds;\n if (preview) return;\n if (\n report.reason === \"periodic\" &&\n report.positionSeconds === lastSavedPosition.current\n ) {\n return;\n }\n const reachedEnd =\n report.reason === \"ended\" ||\n isPlaybackComplete(report.positionSeconds, report.durationSeconds);\n save(\n report.positionSeconds,\n report.durationSeconds,\n reachedEnd && !completedRef.current,\n ).catch((error: unknown) => {\n // Progress is saved again on the next tick; it must not interrupt the video.\n console.warn(\"Saving the progress of the lesson failed\", error);\n });\n }\n\n async function markComplete() {\n setSaving(true);\n try {\n await save(\n lastKnownPosition.current,\n lesson.video?.durationSeconds ?? 0,\n true,\n );\n } catch (error) {\n console.warn(\"Marking the lesson as completed failed\", error);\n } finally {\n setSaving(false);\n }\n }\n\n const description = lesson.description.trim();\n const hasBody = html !== \"\" || description !== \"\";\n\n return (\n <article\n data-lesson-id={lesson.id}\n className=\"min-w-0 space-y-5\"\n aria-labelledby={`lesson-${lesson.id}`}\n >\n {lesson.video && (\n <VideoPlayer\n src={lesson.video.hlsUrl}\n poster={lesson.video.posterUrl}\n lessonId={lesson.id}\n startAt={resumePosition(\n lastKnownPosition.current,\n lesson.video.durationSeconds,\n )}\n durationHint={lesson.video.durationSeconds}\n autoPlay={startPlaying}\n onReport={handleReport}\n onEnded={onEnded}\n onExpired={onUrlsExpired}\n />\n )}\n\n <div className={cn(CARD, \"space-y-5\")}>\n <header className=\"flex flex-wrap items-start justify-between gap-3\">\n <h2\n id={`lesson-${lesson.id}`}\n className=\"text-2xl font-semibold break-words\"\n >\n {lesson.title}\n </h2>\n {!preview && (\n <Button\n size=\"sm\"\n variant={completed ? \"outline\" : \"default\"}\n className={cn(!completed && ACCENT_BUTTON)}\n disabled={completed || saving}\n onClick={markComplete}\n >\n {completed ? texts.markedComplete : texts.markComplete}\n </Button>\n )}\n </header>\n\n {courseType === \"video\" && lesson.video && (\n <label className=\"flex w-fit cursor-pointer items-center gap-2 text-sm text-neutral-600 dark:text-neutral-300\">\n <input\n type=\"checkbox\"\n checked={autoplay}\n onChange={(e) => onAutoplayChange(e.target.checked)}\n />\n {texts.autoplay}\n </label>\n )}\n\n {description !== \"\" && (\n <p className=\"text-sm break-words whitespace-pre-wrap text-neutral-700 dark:text-neutral-300\">\n {description}\n </p>\n )}\n\n {html !== \"\" && (\n <div\n className=\"bitsnap-course-html break-words [&_a]:underline [&_blockquote]:border-l-4 [&_blockquote]:pl-4 [&_h1]:text-2xl [&_h1]:font-semibold [&_h2]:text-xl [&_h2]:font-semibold [&_h3]:text-lg [&_h3]:font-semibold [&_img]:h-auto [&_img]:max-w-full [&_ol]:list-decimal [&_ol]:pl-6 [&_p]:my-3 [&_pre]:overflow-x-auto [&_table]:block [&_table]:max-w-full [&_table]:overflow-x-auto [&_ul]:list-disc [&_ul]:pl-6\"\n // Sanitised by DOMPurify (see sanitize.ts).\n dangerouslySetInnerHTML={{ __html: html }}\n />\n )}\n\n {!hasBody && !lesson.video && (\n <p className=\"text-sm text-neutral-500 dark:text-neutral-400\">\n {texts.noContent}\n </p>\n )}\n\n {lesson.attachments.length > 0 && (\n <section aria-labelledby={`files-${lesson.id}`} className=\"space-y-2\">\n <h3\n id={`files-${lesson.id}`}\n className=\"flex items-center gap-2 text-lg font-semibold\"\n >\n <Paperclip className=\"size-4\" aria-hidden />\n {texts.attachments}\n </h3>\n <ul className=\"space-y-2\">\n {lesson.attachments.map((file) => (\n <li key={file.id}>\n <a\n href={file.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n download={file.name}\n className=\"flex items-center justify-between gap-3 rounded-lg bg-neutral-100 px-3 py-2 text-sm transition hover:bg-neutral-200 dark:bg-neutral-800 dark:hover:bg-neutral-700\"\n >\n <span className=\"min-w-0 break-words\">\n {file.name}\n <span className=\"ml-2 text-xs text-neutral-500 dark:text-neutral-400\">\n {formatBytes(file.size)}\n </span>\n </span>\n <span className=\"flex shrink-0 items-center gap-1 text-xs font-medium\">\n {texts.download}\n <Download className=\"size-3.5\" aria-hidden />\n </span>\n </a>\n </li>\n ))}\n </ul>\n </section>\n )}\n\n <nav className=\"flex items-center justify-between gap-3 border-t pt-4\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={prev == null}\n onClick={() => prev && onNavigate(prev)}\n >\n <ChevronLeft aria-hidden />\n {texts.prevItem}\n </Button>\n <Button\n variant=\"outline\"\n size=\"sm\"\n disabled={next == null}\n onClick={() => next && onNavigate(next)}\n >\n {texts.nextItem}\n <ChevronRight aria-hidden />\n </Button>\n </nav>\n </div>\n\n <div className={cn(CARD)}>\n <LessonRatingBox\n lessonId={lesson.id}\n initial={{\n myRating: lesson.myRating,\n averageRating: lesson.averageRating,\n ratingCount: lesson.ratingCount,\n }}\n />\n </div>\n\n <div className={cn(CARD)}>\n <LessonQuestions lessonId={lesson.id} />\n </div>\n </article>\n );\n}\n","import { useState } from \"react\";\nimport { Button } from \"../ui/button\";\nimport { Textarea } from \"../ui/textarea\";\nimport { useCourseTrack } from \"./analytics\";\nimport { useCourseView } from \"./context\";\nimport { fillTemplate } from \"./logic\";\nimport { RatingStars } from \"./RatingStars\";\nimport type { LessonQuestion, LessonRating } from \"./types\";\nimport { ACCENT_BUTTON, ErrorBox, errorMessage } from \"./ui\";\nimport { useAsyncResource } from \"./useAsyncResource\";\n\n/** Star rating of a lesson with an optional comment. */\nexport function LessonRatingBox({\n lessonId,\n initial,\n}: {\n lessonId: string;\n initial: LessonRating;\n}) {\n const { dataSource, texts, preview } = useCourseView();\n const track = useCourseTrack();\n const [rating, setRating] = useState(initial);\n const [score, setScore] = useState(initial.myRating);\n const [comment, setComment] = useState(\"\");\n const [state, setState] = useState<\"idle\" | \"saving\" | \"saved\" | \"error\">(\n \"idle\",\n );\n const [error, setError] = useState<unknown>();\n\n async function save() {\n if (score < 1) return;\n setState(\"saving\");\n try {\n setRating(await dataSource.rate(lessonId, score, comment.trim()));\n setState(\"saved\");\n track(\"lesson_rated\", { lesson_id: lessonId, rating: score });\n } catch (e) {\n setError(e);\n setState(\"error\");\n }\n }\n\n return (\n <section aria-labelledby={`rate-${lessonId}`} className=\"space-y-3\">\n <h3 id={`rate-${lessonId}`} className=\"text-lg font-semibold\">\n {texts.rateTitle}\n </h3>\n <RatingStars\n value={score}\n onSelect={(n) => {\n setScore(n);\n setState(\"idle\");\n }}\n disabled={preview || state === \"saving\"}\n />\n {rating.ratingCount > 0 && (\n <p className=\"text-xs text-neutral-500 dark:text-neutral-400\">\n {fillTemplate(texts.rateAverage, {\n average: rating.averageRating.toFixed(1),\n count: rating.ratingCount,\n })}\n </p>\n )}\n {!preview && score > 0 && (\n <div className=\"space-y-2\">\n <Textarea\n value={comment}\n maxLength={1000}\n placeholder={texts.ratePlaceholder}\n onChange={(e) => {\n setComment(e.target.value);\n setState(\"idle\");\n }}\n />\n <Button\n size=\"sm\"\n className={ACCENT_BUTTON}\n disabled={state === \"saving\"}\n onClick={save}\n >\n {texts.rateSubmit}\n </Button>\n </div>\n )}\n {state === \"saved\" && (\n <p role=\"status\" className=\"text-sm text-emerald-700 dark:text-emerald-400\">\n {texts.rateSaved}\n </p>\n )}\n {state === \"error\" && (\n <p role=\"alert\" className=\"text-sm text-red-600 dark:text-red-400\">\n {errorMessage(error, texts)}\n </p>\n )}\n </section>\n );\n}\n\nfunction formatDate(date: Date | undefined): string {\n return date == null ? \"\" : date.toLocaleDateString();\n}\n\n/** Questions of the students about a lesson with the ask form. */\nexport function LessonQuestions({ lessonId }: { lessonId: string }) {\n const { dataSource, texts, preview } = useCourseView();\n const questions = useAsyncResource<LessonQuestion[]>(\n () => dataSource.listQuestions(lessonId),\n [dataSource, lessonId],\n );\n const [content, setContent] = useState(\"\");\n const [state, setState] = useState<\"idle\" | \"sending\" | \"sent\" | \"error\">(\n \"idle\",\n );\n const [error, setError] = useState<unknown>();\n\n async function ask() {\n const text = content.trim();\n if (text === \"\") return;\n setState(\"sending\");\n try {\n const created = await dataSource.askQuestion(lessonId, text);\n questions.mutate((list) => [created, ...list]);\n setContent(\"\");\n setState(\"sent\");\n } catch (e) {\n setError(e);\n setState(\"error\");\n }\n }\n\n return (\n <section aria-labelledby={`qa-${lessonId}`} className=\"space-y-4\">\n <h3 id={`qa-${lessonId}`} className=\"text-lg font-semibold\">\n {texts.questionsTitle}\n </h3>\n\n <form\n className=\"space-y-2\"\n onSubmit={(e) => {\n e.preventDefault();\n void ask();\n }}\n >\n <Textarea\n value={content}\n maxLength={2000}\n placeholder={texts.questionPlaceholder}\n aria-label={texts.questionPlaceholder}\n onChange={(e) => {\n setContent(e.target.value);\n setState(\"idle\");\n }}\n />\n <Button\n type=\"submit\"\n size=\"sm\"\n className={ACCENT_BUTTON}\n disabled={state === \"sending\" || content.trim() === \"\"}\n >\n {texts.questionSubmit}\n </Button>\n {state === \"sent\" && (\n <p role=\"status\" className=\"text-sm text-emerald-700 dark:text-emerald-400\">\n {texts.questionSent}\n </p>\n )}\n {state === \"error\" && (\n <p role=\"alert\" className=\"text-sm text-red-600 dark:text-red-400\">\n {errorMessage(error, texts)}\n </p>\n )}\n </form>\n\n {questions.status === \"loading\" && (\n <p role=\"status\" className=\"text-sm text-neutral-500\">\n {texts.loading}\n </p>\n )}\n {questions.status === \"error\" && (\n <ErrorBox error={questions.error} onRetry={questions.reload} />\n )}\n {questions.status === \"success\" &&\n (questions.data.length === 0 ? (\n <p className=\"text-sm text-neutral-500 dark:text-neutral-400\">\n {texts.questionsEmpty}\n </p>\n ) : (\n <ul className=\"space-y-3\">\n {questions.data.map((question) => (\n <li\n key={question.id}\n className=\"rounded-lg bg-neutral-100 p-3 text-sm dark:bg-neutral-800\"\n >\n <p className=\"mb-1 text-xs text-neutral-500 dark:text-neutral-400\">\n {question.isMine ? texts.questionMine : question.authorName}\n {question.createdAt && ` · ${formatDate(question.createdAt)}`}\n </p>\n <p className=\"break-words whitespace-pre-wrap\">{question.content}</p>\n {question.answer && (\n <div className=\"mt-2 border-l-2 border-neutral-400 pl-3 dark:border-neutral-500\">\n <p className=\"mb-1 text-xs font-medium text-neutral-500 dark:text-neutral-400\">\n {texts.questionTeamAnswer}\n {question.answer.createdAt &&\n ` · ${formatDate(question.answer.createdAt)}`}\n </p>\n <p className=\"break-words whitespace-pre-wrap\">\n {question.answer.content}\n </p>\n </div>\n )}\n </li>\n ))}\n </ul>\n ))}\n </section>\n );\n}\n","import { Star } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { useCourseView } from \"./context\";\nimport { fillTemplate } from \"./logic\";\n\n/** Five stars; read-only without `onSelect`. */\nexport function RatingStars({\n value,\n onSelect,\n disabled,\n}: {\n value: number;\n onSelect?: (score: number) => void;\n disabled?: boolean;\n}) {\n const { texts } = useCourseView();\n const [hover, setHover] = useState(0);\n const shown = hover || value;\n\n return (\n <div\n className=\"flex items-center gap-1\"\n role={onSelect ? \"radiogroup\" : \"img\"}\n aria-label={fillTemplate(texts.rateStar, { n: Math.round(value) })}\n >\n {[1, 2, 3, 4, 5].map((n) => {\n const filled = n <= Math.round(shown);\n const star = (\n <Star\n className={cn(\n \"size-5\",\n filled\n ? \"fill-amber-400 text-amber-400\"\n : \"text-neutral-300 dark:text-neutral-600\",\n )}\n aria-hidden\n />\n );\n return onSelect ? (\n <button\n key={n}\n type=\"button\"\n role=\"radio\"\n aria-checked={n === value}\n aria-label={fillTemplate(texts.rateStar, { n })}\n disabled={disabled}\n className=\"rounded p-0.5 outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:opacity-50\"\n onMouseEnter={() => setHover(n)}\n onMouseLeave={() => setHover(0)}\n onClick={() => onSelect(n)}\n >\n {star}\n </button>\n ) : (\n <span key={n}>{star}</span>\n );\n })}\n </div>\n );\n}\n","import { AlertTriangle } from \"lucide-react\";\nimport { cn } from \"../lib/utils\";\nimport { Button } from \"../ui/button\";\nimport { Skeleton } from \"../ui/skeleton\";\nimport { useCourseView } from \"./context\";\nimport type { CourseTexts } from \"./texts\";\nimport { CourseError } from \"./types\";\n\n/** Accent (brand) colors of the course, set as CSS variables by `PanelCourse`. */\nexport const ACCENT_BG =\n \"bg-[var(--course-accent,var(--primary))] text-[var(--course-accent-fg,var(--primary-foreground))] dark:bg-[var(--course-accent-dark,var(--course-accent,var(--primary)))] dark:text-[var(--course-accent-dark-fg,var(--course-accent-fg,var(--primary-foreground)))]\";\n\nexport const ACCENT_BUTTON = cn(\n ACCENT_BG,\n \"hover:bg-[var(--course-accent,var(--primary))] hover:opacity-90 dark:hover:bg-[var(--course-accent-dark,var(--course-accent,var(--primary)))]\",\n);\n\nexport const CARD =\n \"rounded-xl bg-white p-4 drop-shadow-xl sm:p-6 dark:border dark:border-neutral-700 dark:bg-neutral-900 dark:drop-shadow-none\";\n\n/** The explanation of an error that the view can show to the customer. */\nexport function errorMessage(error: unknown, texts: CourseTexts): string {\n if (error instanceof CourseError) {\n switch (error.code) {\n case \"unauthorized\":\n return texts.errorUnauthorized;\n case \"course_not_found\":\n case \"item_not_found\":\n return texts.errorNotFound;\n case \"unavailable\":\n return texts.errorUnavailable;\n }\n }\n return texts.errorGeneric;\n}\n\nexport function ErrorBox({\n error,\n onRetry,\n className,\n}: {\n error: unknown;\n onRetry?: () => void;\n className?: string;\n}) {\n const { texts } = useCourseView();\n return (\n <div\n role=\"alert\"\n className={cn(\n \"flex flex-col items-start gap-3 rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-700 dark:border-red-900 dark:bg-red-950 dark:text-red-300\",\n className,\n )}\n >\n <p className=\"flex items-center gap-2 font-medium\">\n <AlertTriangle className=\"size-4 shrink-0\" aria-hidden />\n {errorMessage(error, texts)}\n </p>\n {onRetry && (\n <Button variant=\"outline\" size=\"sm\" onClick={onRetry}>\n {texts.retry}\n </Button>\n )}\n </div>\n );\n}\n\nexport function LoadingBlock({ label }: { label?: string }) {\n const { texts } = useCourseView();\n return (\n <div role=\"status\" aria-label={label ?? texts.loading} className=\"space-y-3\">\n <Skeleton className=\"aspect-video w-full\" />\n <Skeleton className=\"h-6 w-2/3\" />\n <Skeleton className=\"h-4 w-full\" />\n <Skeleton className=\"h-4 w-5/6\" />\n </div>\n );\n}\n\nexport function ProgressBar({\n percent,\n label,\n}: {\n percent: number;\n label: string;\n}) {\n const value = Math.max(0, Math.min(100, Math.round(percent)));\n return (\n <div\n role=\"progressbar\"\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={value}\n aria-label={label}\n className=\"h-2 w-full overflow-hidden rounded-full bg-neutral-200 dark:bg-neutral-700\"\n >\n <div\n className={cn(\"h-full rounded-full transition-all duration-300\", ACCENT_BG)}\n style={{ width: `${value}%` }}\n />\n </div>\n );\n}\n","import { useCallback, useEffect, useRef, useState } from \"react\";\n\nexport type AsyncResource<T> =\n | { status: \"loading\"; data?: undefined; error?: undefined }\n | { status: \"success\"; data: T; error?: undefined }\n | { status: \"error\"; data?: undefined; error: unknown };\n\n/**\n * Loads a value on the client when `deps` change. `reload` shows the loading\n * state again, `refresh` replaces the data quietly, `mutate` edits it locally.\n * An answer to an outdated request is dropped.\n */\nexport function useAsyncResource<T>(\n load: () => Promise<T>,\n deps: readonly unknown[],\n initialData?: T,\n): AsyncResource<T> & {\n reload: () => void;\n refresh: () => void;\n mutate: (update: (data: T) => T) => void;\n} {\n const [state, setState] = useState<AsyncResource<T>>(\n initialData !== undefined\n ? { status: \"success\", data: initialData }\n : { status: \"loading\" },\n );\n const [reloadKey, setReloadKey] = useState(0);\n const requestId = useRef(0);\n const loadRef = useRef(load);\n loadRef.current = load;\n const skipFirst = useRef(initialData !== undefined);\n\n useEffect(() => {\n if (skipFirst.current) {\n skipFirst.current = false;\n return;\n }\n const id = ++requestId.current;\n setState({ status: \"loading\" });\n loadRef\n .current()\n .then((data) => {\n if (id === requestId.current) setState({ status: \"success\", data });\n })\n .catch((error: unknown) => {\n if (id === requestId.current) setState({ status: \"error\", error });\n });\n return () => {\n requestId.current += 1;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [...deps, reloadKey]);\n\n const reload = useCallback(() => setReloadKey((key) => key + 1), []);\n\n const refresh = useCallback(() => {\n const id = ++requestId.current;\n loadRef\n .current()\n .then((data) => {\n if (id === requestId.current) setState({ status: \"success\", data });\n })\n .catch(() => {\n // Keep what is on screen; a quiet refresh must not replace it with an error.\n });\n }, []);\n\n const mutate = useCallback((update: (data: T) => T) => {\n setState((current) =>\n current.status === \"success\"\n ? { status: \"success\", data: update(current.data) }\n : current,\n );\n }, []);\n\n return { ...state, reload, refresh, mutate } as AsyncResource<T> & {\n reload: () => void;\n refresh: () => void;\n mutate: (update: (data: T) => T) => void;\n };\n}\n","import DOMPurify from \"dompurify\";\n\n/** The part of DOMPurify the wrapper uses, so tests can bring their own DOM. */\nexport type PurifyLike = {\n isSupported?: boolean;\n sanitize?: (html: string, config: Record<string, unknown>) => unknown;\n addHook?: (\n name: \"afterSanitizeAttributes\",\n hook: (node: Element) => void,\n ) => void;\n};\n\nconst SANITIZE_CONFIG = {\n USE_PROFILES: { html: true },\n // Lesson text is content, not a page: no embedded documents, no forms and\n // no inline styles (they allow overlays on top of the panel).\n FORBID_TAGS: [\n \"style\",\n \"form\",\n \"input\",\n \"button\",\n \"textarea\",\n \"select\",\n \"iframe\",\n \"frame\",\n \"object\",\n \"embed\",\n \"base\",\n \"meta\",\n \"link\",\n ],\n FORBID_ATTR: [\"style\", \"srcdoc\", \"formaction\"],\n ALLOW_DATA_ATTR: false,\n RETURN_TRUSTED_TYPE: false,\n};\n\n/**\n * Builds a sanitizer around a DOMPurify instance. Links open in a new tab\n * without `opener`, images load lazily. Without a DOM (server rendering) the\n * sanitizer returns an empty string rather than unsanitised HTML.\n */\nexport function createHtmlSanitizer(\n purify: PurifyLike,\n): (html: string | null | undefined) => string {\n if (purify.isSupported === false || purify.sanitize == null) {\n return () => \"\";\n }\n const sanitize = purify.sanitize.bind(purify);\n\n purify.addHook?.(\"afterSanitizeAttributes\", (node) => {\n if (node.tagName === \"A\" && node.hasAttribute(\"href\")) {\n node.setAttribute(\"target\", \"_blank\");\n node.setAttribute(\"rel\", \"noopener noreferrer nofollow\");\n }\n if (node.tagName === \"IMG\") {\n node.setAttribute(\"loading\", \"lazy\");\n node.setAttribute(\"referrerpolicy\", \"no-referrer\");\n }\n });\n\n return (html) => {\n if (html == null || html === \"\") {\n return \"\";\n }\n return String(sanitize(html, SANITIZE_CONFIG));\n };\n}\n\nlet sharedSanitizer: ((html: string | null | undefined) => string) | undefined;\n\n/**\n * Sanitizes the HTML of a lesson with the DOM of the browser. Returns an empty\n * string when there is no DOM, so call it on the client (inside an effect or\n * `useMemo` of a component that only renders content after loading).\n */\nexport function sanitizeLessonHtml(html: string | null | undefined): string {\n if (typeof window === \"undefined\") {\n return \"\";\n }\n sharedSanitizer ??= createHtmlSanitizer(DOMPurify as unknown as PurifyLike);\n return sharedSanitizer(html);\n}\n","import Hls from \"hls.js\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Button } from \"../ui/button\";\nimport { newMilestones } from \"@/src/analytics/instrument\";\nimport { useCourseTrack } from \"./analytics\";\nimport { useCourseView } from \"./context\";\nimport { PROGRESS_SAVE_INTERVAL_MS } from \"./logic\";\n\nexport type PlaybackReport = {\n positionSeconds: number;\n durationSeconds: number;\n reason: \"periodic\" | \"pause\" | \"ended\" | \"leave\";\n};\n\ntype VideoPlayerProps = {\n /** HLS manifest URL. */\n src: string;\n poster?: string;\n /** Position to resume from, seconds. */\n startAt?: number;\n /** Length known from the lesson, used until the player knows it. */\n durationHint?: number;\n autoPlay?: boolean;\n /** Lesson the video belongs to; enables the `video_progress` statistics. */\n lessonId?: string;\n /** Reports the position every ~15 s while playing, on pause and at the end. */\n onReport?: (report: PlaybackReport) => void;\n onEnded?: () => void;\n /** The signed URL was refused; the owner should load a fresh one. */\n onExpired?: () => void;\n className?: string;\n};\n\nfunction canPlayNativeHls(video: HTMLVideoElement): boolean {\n return video.canPlayType(\"application/vnd.apple.mpegurl\") !== \"\";\n}\n\n/**\n * HLS player: hls.js where there is Media Source support, the native player\n * of Safari otherwise.\n */\nexport function VideoPlayer({\n src,\n poster,\n startAt = 0,\n durationHint = 0,\n autoPlay = false,\n lessonId,\n onReport,\n onEnded,\n onExpired,\n className,\n}: VideoPlayerProps) {\n const { texts } = useCourseView();\n const track = useCourseTrack();\n const trackRef = useRef(track);\n trackRef.current = track;\n const videoRef = useRef<HTMLVideoElement>(null);\n const [failed, setFailed] = useState(false);\n const [attempt, setAttempt] = useState(0);\n\n // Callbacks change on every render of the owner; the player must not reload.\n const callbacks = useRef({ onReport, onEnded, onExpired, durationHint });\n callbacks.current = { onReport, onEnded, onExpired, durationHint };\n const lastReported = useRef<number | undefined>(undefined);\n\n // React clears `videoRef` before the effect cleanup runs, so the cleanup\n // passes the element it captured.\n const report = useCallback((reason: PlaybackReport[\"reason\"], video = videoRef.current) => {\n if (video == null || callbacks.current.onReport == null) {\n return;\n }\n const positionSeconds = Math.floor(video.currentTime);\n if (positionSeconds <= 0 && reason !== \"ended\") {\n return;\n }\n const duration = Number.isFinite(video.duration)\n ? video.duration\n : callbacks.current.durationHint;\n lastReported.current = positionSeconds;\n callbacks.current.onReport({\n positionSeconds,\n durationSeconds: Math.floor(duration || 0),\n reason,\n });\n }, []);\n\n useEffect(() => {\n const video = videoRef.current;\n if (video == null) {\n return;\n }\n setFailed(false);\n let hls: Hls | undefined;\n let retriedNetwork = false;\n let retriedMedia = false;\n let timer: ReturnType<typeof setInterval> | undefined;\n\n const start = () => {\n if (startAt > 0) {\n video.currentTime = startAt;\n }\n if (autoPlay) {\n void video.play().catch(() => {\n // The browser blocked autoplay; the viewer presses play.\n });\n }\n };\n\n if (canPlayNativeHls(video)) {\n video.src = src;\n video.addEventListener(\"loadedmetadata\", start, { once: true });\n } else if (Hls.isSupported()) {\n hls = new Hls();\n hls.attachMedia(video);\n hls.loadSource(src);\n hls.on(Hls.Events.MANIFEST_PARSED, start);\n hls.on(Hls.Events.ERROR, (_event, data) => {\n if (!data.fatal) {\n return;\n }\n const status = data.response?.code;\n if (status === 401 || status === 403) {\n callbacks.current.onExpired?.();\n setFailed(true);\n } else if (data.type === Hls.ErrorTypes.NETWORK_ERROR && !retriedNetwork) {\n retriedNetwork = true;\n hls?.startLoad();\n } else if (data.type === Hls.ErrorTypes.MEDIA_ERROR && !retriedMedia) {\n retriedMedia = true;\n hls?.recoverMediaError();\n } else {\n setFailed(true);\n }\n });\n } else {\n video.src = src;\n }\n\n const onPlay = () => {\n clearInterval(timer);\n timer = setInterval(() => report(\"periodic\"), PROGRESS_SAVE_INTERVAL_MS);\n };\n const onPause = () => {\n clearInterval(timer);\n // `ended` has its own report.\n if (!video.ended) report(\"pause\");\n };\n // Milestones count only what was played through: a jump forward does not\n // report the parts that were skipped, and each one is sent once per view.\n const milestones = new Set<number>();\n let lastTime = video.currentTime;\n let watchedTo = 0;\n const sendMilestones = (percent: number) => {\n if (lessonId == null) {\n return;\n }\n for (const milestone of newMilestones(percent, milestones)) {\n trackRef.current(\"video_progress\", { lesson_id: lessonId, milestone });\n }\n };\n const onTimeUpdate = () => {\n const now = video.currentTime;\n const duration = Number.isFinite(video.duration)\n ? video.duration\n : callbacks.current.durationHint;\n if (duration > 0 && !video.seeking && now >= lastTime && now - lastTime <= 2) {\n watchedTo = Math.max(watchedTo, now);\n sendMilestones((watchedTo / duration) * 100);\n }\n lastTime = now;\n };\n const onVideoEnded = () => {\n clearInterval(timer);\n // Reaching the end counts even after a jump, but only as 100.\n if (lessonId != null && !milestones.has(100)) {\n milestones.add(100);\n trackRef.current(\"video_progress\", { lesson_id: lessonId, milestone: 100 });\n }\n report(\"ended\");\n callbacks.current.onEnded?.();\n };\n const onVideoError = () => {\n if (hls == null) {\n setFailed(true);\n }\n };\n\n video.addEventListener(\"play\", onPlay);\n video.addEventListener(\"pause\", onPause);\n video.addEventListener(\"ended\", onVideoEnded);\n video.addEventListener(\"error\", onVideoError);\n video.addEventListener(\"timeupdate\", onTimeUpdate);\n\n return () => {\n clearInterval(timer);\n video.removeEventListener(\"play\", onPlay);\n video.removeEventListener(\"pause\", onPause);\n video.removeEventListener(\"ended\", onVideoEnded);\n video.removeEventListener(\"error\", onVideoError);\n video.removeEventListener(\"timeupdate\", onTimeUpdate);\n video.removeEventListener(\"loadedmetadata\", start);\n // Leaving the lesson mid-video keeps the position.\n if (!video.ended && video.currentTime > 0) report(\"leave\", video);\n hls?.destroy();\n video.removeAttribute(\"src\");\n video.load();\n };\n // `startAt` and `autoPlay` only matter when a video starts loading.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [src, attempt]);\n\n return (\n <div className={cn(\"relative overflow-hidden rounded-xl bg-black\", className)}>\n <video\n ref={videoRef}\n className=\"aspect-video w-full\"\n poster={poster}\n controls\n playsInline\n preload=\"metadata\"\n controlsList=\"nodownload\"\n />\n {failed && (\n <div\n role=\"alert\"\n className=\"absolute inset-0 flex flex-col items-center justify-center gap-3 bg-black/80 p-4 text-center text-sm text-white\"\n >\n <p>{texts.videoError}</p>\n <Button\n variant=\"secondary\"\n size=\"sm\"\n onClick={() => {\n callbacks.current.onExpired?.();\n setAttempt((n) => n + 1);\n }}\n >\n {texts.videoRetry}\n </Button>\n </div>\n )}\n </div>\n );\n}\n","import { getLocale } from \"../components/checkout/locale\";\nimport { resolveTexts, type LocaleTables } from \"../lib/texts\";\n\n/**\n * Every string of the course view. Pass a partial object through the `texts`\n * prop of `PanelCourse` to override any of them, whatever the locale.\n * Placeholders like `{count}` stay in overrides.\n */\nexport type CourseTexts = {\n back: string;\n loading: string;\n retry: string;\n errorGeneric: string;\n errorUnauthorized: string;\n errorNotFound: string;\n errorUnavailable: string;\n previewBadge: string;\n /** Placeholders `{done}` and `{total}`. */\n progressLessons: string;\n /** Placeholder `{percent}`. */\n progressPercent: string;\n contents: string;\n free: string;\n locked: string;\n /** Placeholder `{test}`. */\n lockedByTest: string;\n /** Placeholder `{test}`. */\n lockedPage: string;\n testBadge: string;\n completed: string;\n continueCourse: string;\n noItem: string;\n // Lesson\n markComplete: string;\n markedComplete: string;\n prevItem: string;\n nextItem: string;\n attachments: string;\n download: string;\n autoplay: string;\n videoError: string;\n videoRetry: string;\n noContent: string;\n // Test\n testGating: string;\n testAttemptsLeft: string;\n testAttemptsUnlimited: string;\n testNoAttemptsLeft: string;\n testPassThreshold: string;\n testBestScore: string;\n testSubmit: string;\n testSubmitting: string;\n /** Placeholder `{count}`. */\n testUnanswered: string;\n testPassed: string;\n testFailed: string;\n /** Placeholder `{percent}`. */\n testScore: string;\n testCorrect: string;\n testIncorrect: string;\n testCorrectAnswer: string;\n testTryAgain: string;\n testPreviousAttempts: string;\n /** Placeholders `{percent}` and `{date}`. */\n testAttemptRow: string;\n testNoQuestions: string;\n mcqSingle: string;\n mcqMultiple: string;\n todoHint: string;\n todoRequired: string;\n gapPlaceholder: string;\n sortHint: string;\n moveUp: string;\n moveDown: string;\n // Questions and ratings\n questionsTitle: string;\n questionsEmpty: string;\n questionPlaceholder: string;\n questionSubmit: string;\n questionSent: string;\n questionMine: string;\n questionTeamAnswer: string;\n rateTitle: string;\n /** Placeholder `{n}`. */\n rateStar: string;\n ratePlaceholder: string;\n rateSubmit: string;\n rateSaved: string;\n /** Placeholders `{average}` and `{count}`. */\n rateAverage: string;\n};\n\nexport const COURSE_TEXT_TABLES: LocaleTables<keyof CourseTexts> = {\n pl: {\n back: \"Wróć\",\n loading: \"Ładowanie kursu...\",\n retry: \"Spróbuj ponownie\",\n errorGeneric: \"Nie udało się załadować danych. Spróbuj ponownie za chwilę.\",\n errorUnauthorized:\n \"Nie masz dostępu do tego kursu. Zaloguj się ponownie lub skontaktuj się z pomocą.\",\n errorNotFound: \"Nie znaleziono tego kursu lub lekcji.\",\n errorUnavailable:\n \"Kurs jest chwilowo niedostępny. Spróbuj ponownie za chwilę.\",\n previewBadge: \"Podgląd\",\n progressLessons: \"{done} z {total} lekcji\",\n progressPercent: \"{percent}% ukończono\",\n contents: \"Spis treści\",\n free: \"Darmowa\",\n locked: \"Zablokowane\",\n lockedByTest: \"Odblokuje je zaliczenie testu: {test}\",\n lockedPage:\n \"Ta lekcja jest zablokowana. Zalicz test „{test}”, aby ją odblokować.\",\n testBadge: \"Test\",\n completed: \"Ukończono\",\n continueCourse: \"Kontynuuj kurs\",\n noItem: \"Wybierz lekcję z listy.\",\n markComplete: \"Oznacz jako ukończoną\",\n markedComplete: \"Lekcja ukończona\",\n prevItem: \"Poprzednia\",\n nextItem: \"Następna\",\n attachments: \"Materiały do pobrania\",\n download: \"Pobierz\",\n autoplay: \"Autoodtwarzanie następnej lekcji\",\n videoError: \"Nie udało się odtworzyć wideo.\",\n videoRetry: \"Odśwież wideo\",\n noContent: \"Ta lekcja nie ma jeszcze treści.\",\n testGating: \"Zaliczenie tego testu odblokowuje kolejne lekcje.\",\n testAttemptsLeft: \"Pozostało prób: {count}\",\n testAttemptsUnlimited: \"Liczba prób nie jest ograniczona.\",\n testNoAttemptsLeft: \"Wykorzystano wszystkie próby.\",\n testPassThreshold: \"Próg zaliczenia: {percent}%\",\n testBestScore: \"Najlepszy wynik: {percent}%\",\n testSubmit: \"Sprawdź odpowiedzi\",\n testSubmitting: \"Sprawdzanie...\",\n testUnanswered: \"Pytania bez odpowiedzi: {count}\",\n testPassed: \"Test zaliczony\",\n testFailed: \"Test niezaliczony\",\n testScore: \"Wynik: {percent}%\",\n testCorrect: \"Poprawnie\",\n testIncorrect: \"Niepoprawnie\",\n testCorrectAnswer: \"Poprawna odpowiedź\",\n testTryAgain: \"Spróbuj ponownie\",\n testPreviousAttempts: \"Poprzednie podejścia\",\n testAttemptRow: \"{percent}% · {date}\",\n testNoQuestions: \"Ten test nie ma jeszcze pytań.\",\n mcqSingle: \"Wybierz jedną odpowiedź\",\n mcqMultiple: \"Zaznacz wszystkie poprawne odpowiedzi\",\n todoHint: \"Zaznacz wykonane kroki\",\n todoRequired: \"wymagany\",\n gapPlaceholder: \"uzupełnij\",\n sortHint: \"Ułóż elementy w poprawnej kolejności\",\n moveUp: \"Przesuń w górę\",\n moveDown: \"Przesuń w dół\",\n questionsTitle: \"Pytania do lekcji\",\n questionsEmpty: \"Nikt jeszcze nie zadał pytania.\",\n questionPlaceholder: \"Zadaj pytanie autorowi kursu\",\n questionSubmit: \"Wyślij pytanie\",\n questionSent: \"Pytanie wysłane.\",\n questionMine: \"Ty\",\n questionTeamAnswer: \"Odpowiedź autora\",\n rateTitle: \"Oceń lekcję\",\n rateStar: \"{n} z 5\",\n ratePlaceholder: \"Komentarz (opcjonalnie)\",\n rateSubmit: \"Zapisz ocenę\",\n rateSaved: \"Dziękujemy za ocenę.\",\n rateAverage: \"Średnia ocena {average} ({count})\",\n },\n en: {\n back: \"Back\",\n loading: \"Loading the course...\",\n retry: \"Try again\",\n errorGeneric: \"Could not load the data. Please try again in a moment.\",\n errorUnauthorized:\n \"You do not have access to this course. Sign in again or contact support.\",\n errorNotFound: \"This course or lesson was not found.\",\n errorUnavailable:\n \"The course is temporarily unavailable. Please try again in a moment.\",\n previewBadge: \"Preview\",\n progressLessons: \"{done} of {total} lessons\",\n progressPercent: \"{percent}% complete\",\n contents: \"Contents\",\n free: \"Free\",\n locked: \"Locked\",\n lockedByTest: \"Pass the test to unlock it: {test}\",\n lockedPage: \"This lesson is locked. Pass the test “{test}” to unlock it.\",\n testBadge: \"Test\",\n completed: \"Completed\",\n continueCourse: \"Continue the course\",\n noItem: \"Pick a lesson from the list.\",\n markComplete: \"Mark as completed\",\n markedComplete: \"Lesson completed\",\n prevItem: \"Previous\",\n nextItem: \"Next\",\n attachments: \"Downloads\",\n download: \"Download\",\n autoplay: \"Autoplay the next lesson\",\n videoError: \"The video could not be played.\",\n videoRetry: \"Reload the video\",\n noContent: \"This lesson has no content yet.\",\n testGating: \"Passing this test unlocks the following lessons.\",\n testAttemptsLeft: \"Attempts left: {count}\",\n testAttemptsUnlimited: \"The number of attempts is unlimited.\",\n testNoAttemptsLeft: \"You have used all attempts.\",\n testPassThreshold: \"Passing score: {percent}%\",\n testBestScore: \"Best score: {percent}%\",\n testSubmit: \"Check answers\",\n testSubmitting: \"Checking...\",\n testUnanswered: \"Unanswered questions: {count}\",\n testPassed: \"Test passed\",\n testFailed: \"Test not passed\",\n testScore: \"Score: {percent}%\",\n testCorrect: \"Correct\",\n testIncorrect: \"Incorrect\",\n testCorrectAnswer: \"Correct answer\",\n testTryAgain: \"Try again\",\n testPreviousAttempts: \"Previous attempts\",\n testAttemptRow: \"{percent}% · {date}\",\n testNoQuestions: \"This test has no questions yet.\",\n mcqSingle: \"Choose one answer\",\n mcqMultiple: \"Select all correct answers\",\n todoHint: \"Check the steps you have done\",\n todoRequired: \"required\",\n gapPlaceholder: \"fill in\",\n sortHint: \"Put the items in the correct order\",\n moveUp: \"Move up\",\n moveDown: \"Move down\",\n questionsTitle: \"Questions about this lesson\",\n questionsEmpty: \"Nobody has asked a question yet.\",\n questionPlaceholder: \"Ask the course author a question\",\n questionSubmit: \"Send question\",\n questionSent: \"Question sent.\",\n questionMine: \"You\",\n questionTeamAnswer: \"Answer from the author\",\n rateTitle: \"Rate this lesson\",\n rateStar: \"{n} of 5\",\n ratePlaceholder: \"Comment (optional)\",\n rateSubmit: \"Save rating\",\n rateSaved: \"Thanks for your rating.\",\n rateAverage: \"Average rating {average} ({count})\",\n },\n};\n\n/** Course strings for a locale (library locale by default) with overrides. */\nexport function getCourseTexts(\n overrides?: Partial<CourseTexts>,\n locale: string = getLocale(),\n): CourseTexts {\n return resolveTexts(COURSE_TEXT_TABLES, locale, overrides);\n}\n","import { ArrowDown, ArrowUp, Check, X } from \"lucide-react\";\nimport { useRef, useState } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Button } from \"../ui/button\";\nimport { Input } from \"../ui/input\";\nimport { scoreBucket } from \"@/src/analytics/instrument\";\nimport { useCourseTrack } from \"./analytics\";\nimport { useCourseView } from \"./context\";\nimport {\n countUnanswered,\n describeAnswer,\n emptyAnswer,\n fillTemplate,\n moveItem,\n splitGapTemplate,\n} from \"./logic\";\nimport type {\n QuestionAnswer,\n Test,\n TestAnswers,\n TestQuestion,\n TestResult,\n TestSubmissionList,\n} from \"./types\";\nimport { ACCENT_BUTTON, CARD, ErrorBox, LoadingBlock, errorMessage } from \"./ui\";\nimport { useAsyncResource } from \"./useAsyncResource\";\n\ntype AnswerEditorProps<A extends QuestionAnswer> = {\n question: TestQuestion;\n answer: A;\n disabled: boolean;\n onChange: (answer: A) => void;\n};\n\nfunction McqEditor({\n question,\n answer,\n disabled,\n onChange,\n}: AnswerEditorProps<Extract<QuestionAnswer, { type: \"mcq\" }>>) {\n const { texts } = useCourseView();\n if (question.config.type !== \"mcq\") return null;\n const { choices, multiple } = question.config;\n return (\n <fieldset className=\"space-y-2\" disabled={disabled}>\n <legend className=\"mb-2 text-xs text-neutral-500 dark:text-neutral-400\">\n {multiple ? texts.mcqMultiple : texts.mcqSingle}\n </legend>\n {choices.map((choice) => {\n const checked = answer.selectedIds.includes(choice.id);\n return (\n <label\n key={choice.id}\n className=\"flex cursor-pointer items-start gap-3 rounded-lg bg-neutral-100 px-3 py-2 text-sm dark:bg-neutral-800\"\n >\n <input\n className=\"mt-0.5\"\n type={multiple ? \"checkbox\" : \"radio\"}\n name={`q-${question.id}`}\n checked={checked}\n onChange={() =>\n onChange({\n type: \"mcq\",\n selectedIds: multiple\n ? checked\n ? answer.selectedIds.filter((id) => id !== choice.id)\n : [...answer.selectedIds, choice.id]\n : [choice.id],\n })\n }\n />\n <span className=\"break-words\">{choice.label}</span>\n </label>\n );\n })}\n </fieldset>\n );\n}\n\nfunction TodolistEditor({\n question,\n answer,\n disabled,\n onChange,\n}: AnswerEditorProps<Extract<QuestionAnswer, { type: \"todolist\" }>>) {\n const { texts } = useCourseView();\n if (question.config.type !== \"todolist\") return null;\n return (\n <fieldset className=\"space-y-2\" disabled={disabled}>\n <legend className=\"mb-2 text-xs text-neutral-500 dark:text-neutral-400\">\n {texts.todoHint}\n </legend>\n {question.config.steps.map((step) => {\n const checked = answer.checkedIds.includes(step.id);\n return (\n <label\n key={step.id}\n className=\"flex cursor-pointer items-start gap-3 rounded-lg bg-neutral-100 px-3 py-2 text-sm dark:bg-neutral-800\"\n >\n <input\n className=\"mt-0.5\"\n type=\"checkbox\"\n checked={checked}\n onChange={() =>\n onChange({\n type: \"todolist\",\n checkedIds: checked\n ? answer.checkedIds.filter((id) => id !== step.id)\n : [...answer.checkedIds, step.id],\n })\n }\n />\n <span className=\"break-words\">\n {step.label}\n {step.required && (\n <span className=\"ml-2 text-xs text-neutral-500 dark:text-neutral-400\">\n ({texts.todoRequired})\n </span>\n )}\n </span>\n </label>\n );\n })}\n </fieldset>\n );\n}\n\nfunction FillGapEditor({\n question,\n answer,\n disabled,\n onChange,\n}: AnswerEditorProps<Extract<QuestionAnswer, { type: \"fill_gap\" }>>) {\n const { texts } = useCourseView();\n if (question.config.type !== \"fill_gap\") return null;\n const segments = splitGapTemplate(\n question.config.template,\n question.config.blanks,\n );\n return (\n <p className=\"text-sm leading-10 break-words\">\n {segments.map((segment, index) =>\n segment.type === \"text\" ? (\n <span key={index} className=\"whitespace-pre-wrap\">\n {segment.text}\n </span>\n ) : (\n <Input\n key={index}\n className=\"mx-1 inline-block h-8 w-36 align-middle\"\n aria-label={`${texts.gapPlaceholder} ${segment.id}`}\n placeholder={texts.gapPlaceholder}\n disabled={disabled}\n value={answer.values[segment.id] ?? \"\"}\n onChange={(e) =>\n onChange({\n type: \"fill_gap\",\n values: { ...answer.values, [segment.id]: e.target.value },\n })\n }\n />\n ),\n )}\n </p>\n );\n}\n\nfunction SortEditor({\n question,\n answer,\n disabled,\n onChange,\n}: AnswerEditorProps<Extract<QuestionAnswer, { type: \"sort_order\" }>>) {\n const { texts } = useCourseView();\n if (question.config.type !== \"sort_order\") return null;\n const labels = new Map(question.config.items.map((i) => [i.id, i.label]));\n const move = (from: number, to: number) =>\n onChange({\n type: \"sort_order\",\n orderedIds: moveItem(answer.orderedIds, from, to),\n });\n return (\n <div className=\"space-y-2\">\n <p className=\"text-xs text-neutral-500 dark:text-neutral-400\">\n {texts.sortHint}\n </p>\n <ol className=\"space-y-2\">\n {answer.orderedIds.map((id, index) => (\n <li\n key={id}\n className=\"flex items-center gap-2 rounded-lg bg-neutral-100 px-3 py-2 text-sm dark:bg-neutral-800\"\n >\n <span className=\"w-5 text-xs text-neutral-500\">{index + 1}.</span>\n <span className=\"min-w-0 flex-1 break-words\">{labels.get(id) ?? id}</span>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n aria-label={texts.moveUp}\n disabled={disabled || index === 0}\n onClick={() => move(index, index - 1)}\n >\n <ArrowUp aria-hidden />\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n aria-label={texts.moveDown}\n disabled={disabled || index === answer.orderedIds.length - 1}\n onClick={() => move(index, index + 1)}\n >\n <ArrowDown aria-hidden />\n </Button>\n </li>\n ))}\n </ol>\n </div>\n );\n}\n\nfunction QuestionEditor({\n question,\n answer,\n disabled,\n onChange,\n}: {\n question: TestQuestion;\n answer: QuestionAnswer;\n disabled: boolean;\n onChange: (answer: QuestionAnswer) => void;\n}) {\n const common = { question, disabled, onChange };\n switch (answer.type) {\n case \"mcq\":\n return <McqEditor {...common} answer={answer} />;\n case \"todolist\":\n return <TodolistEditor {...common} answer={answer} />;\n case \"fill_gap\":\n return <FillGapEditor {...common} answer={answer} />;\n case \"sort_order\":\n return <SortEditor {...common} answer={answer} />;\n }\n}\n\nfunction SubmissionHistory({ list }: { list: TestSubmissionList }) {\n const { texts } = useCourseView();\n if (list.submissions.length === 0) return null;\n return (\n <section className=\"space-y-2\">\n <h3 className=\"text-sm font-semibold\">{texts.testPreviousAttempts}</h3>\n <ul className=\"space-y-1 text-sm\">\n {list.submissions.map((submission) => (\n <li\n key={submission.id}\n className=\"flex items-center gap-2 text-neutral-700 dark:text-neutral-300\"\n >\n {submission.passed ? (\n <Check className=\"size-4 text-emerald-600\" aria-hidden />\n ) : (\n <X className=\"size-4 text-red-600\" aria-hidden />\n )}\n {fillTemplate(texts.testAttemptRow, {\n percent: submission.scorePercent,\n date: submission.createdAt?.toLocaleString() ?? \"\",\n })}\n </li>\n ))}\n </ul>\n </section>\n );\n}\n\ntype TestFormProps = {\n test: Test;\n onSubmitted: (result: TestResult) => void;\n onTryAgain: () => void;\n};\n\nfunction TestForm({ test, onSubmitted, onTryAgain }: TestFormProps) {\n const { dataSource, texts } = useCourseView();\n const [answers, setAnswers] = useState<TestAnswers>({});\n const [result, setResult] = useState<TestResult>();\n const [submitting, setSubmitting] = useState(false);\n const [error, setError] = useState<unknown>();\n const startedAt = useRef(Date.now());\n // The questions as they were shown: a refreshed test would shuffle again.\n const questions = useRef(test.questions).current;\n\n const outOfAttempts = test.attemptsLeft != null && test.attemptsLeft <= 0;\n const attemptsLeft = result ? result.attemptsLeft : test.attemptsLeft;\n const unanswered = countUnanswered(questions, answers);\n\n const answerOf = (q: TestQuestion): QuestionAnswer =>\n answers[q.id] ?? emptyAnswer(q.config);\n\n async function submit() {\n setSubmitting(true);\n setError(undefined);\n try {\n // The shuffled order of a sort question that was not touched is an answer too.\n const complete: TestAnswers = {};\n for (const question of questions) {\n complete[question.id] = answerOf(question);\n }\n const submitted = await dataSource.submitTest(\n test.id,\n complete,\n Date.now() - startedAt.current,\n );\n setResult(submitted);\n onSubmitted(submitted);\n } catch (e) {\n setError(e);\n } finally {\n setSubmitting(false);\n }\n }\n\n const resultByQuestion = new Map(result?.results.map((r) => [r.questionId, r]));\n\n if (questions.length === 0) {\n return (\n <p className=\"text-sm text-neutral-500 dark:text-neutral-400\">\n {texts.testNoQuestions}\n </p>\n );\n }\n\n return (\n <form\n className=\"space-y-6\"\n onSubmit={(e) => {\n e.preventDefault();\n if (result == null && !outOfAttempts) void submit();\n }}\n >\n {result && (\n <div\n role=\"status\"\n className={cn(\n \"rounded-xl p-4 text-sm\",\n result.submission.passed\n ? \"bg-emerald-50 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300\"\n : \"bg-red-50 text-red-800 dark:bg-red-950 dark:text-red-300\",\n )}\n >\n <p className=\"text-base font-semibold\">\n {result.submission.passed ? texts.testPassed : texts.testFailed}\n </p>\n <p>\n {fillTemplate(texts.testScore, {\n percent: result.submission.scorePercent,\n })}\n </p>\n </div>\n )}\n\n <ol className=\"space-y-6\">\n {questions.map((question, index) => {\n const questionResult = resultByQuestion.get(question.id);\n const correctText =\n questionResult?.correctAnswer != null\n ? describeAnswer(question, questionResult.correctAnswer)\n : \"\";\n return (\n <li key={question.id} className=\"space-y-3\" data-question-id={question.id}>\n <p className=\"font-medium break-words\">\n {index + 1}. {question.prompt}\n </p>\n <QuestionEditor\n question={question}\n answer={answerOf(question)}\n disabled={result != null || submitting || outOfAttempts}\n onChange={(answer) =>\n setAnswers((current) => ({ ...current, [question.id]: answer }))\n }\n />\n {questionResult && (\n <div\n className={cn(\n \"flex items-start gap-2 text-sm\",\n questionResult.correct\n ? \"text-emerald-700 dark:text-emerald-400\"\n : \"text-red-700 dark:text-red-400\",\n )}\n >\n {questionResult.correct ? (\n <Check className=\"mt-0.5 size-4 shrink-0\" aria-hidden />\n ) : (\n <X className=\"mt-0.5 size-4 shrink-0\" aria-hidden />\n )}\n <span>\n {questionResult.correct ? texts.testCorrect : texts.testIncorrect}\n {correctText !== \"\" && !questionResult.correct && (\n <span className=\"block text-neutral-700 dark:text-neutral-300\">\n {texts.testCorrectAnswer}: {correctText}\n </span>\n )}\n </span>\n </div>\n )}\n </li>\n );\n })}\n </ol>\n\n {error != null && (\n <p role=\"alert\" className=\"text-sm text-red-600 dark:text-red-400\">\n {errorMessage(error, texts)}\n </p>\n )}\n\n {result == null ? (\n <div className=\"space-y-2\">\n {unanswered > 0 && !outOfAttempts && (\n <p className=\"text-xs text-neutral-500 dark:text-neutral-400\">\n {fillTemplate(texts.testUnanswered, { count: unanswered })}\n </p>\n )}\n <Button\n type=\"submit\"\n className={ACCENT_BUTTON}\n disabled={submitting || outOfAttempts}\n >\n {submitting ? texts.testSubmitting : texts.testSubmit}\n </Button>\n </div>\n ) : (\n attemptsLeft !== 0 &&\n !result.submission.passed && (\n <Button type=\"button\" variant=\"outline\" onClick={onTryAgain}>\n {texts.testTryAgain}\n </Button>\n )\n )}\n </form>\n );\n}\n\n/** A test: intro, questions, submission and result. */\nexport function TestPane({\n testId,\n title,\n onSubmitted,\n}: {\n testId: string;\n /** Title from the course structure, shown while the test loads. The owner gives this component a `key` of the test id. */\n title: string;\n /** Called after a submission so the owner can reload the unlocked structure. */\n onSubmitted: (result: TestResult) => void;\n}) {\n const { dataSource, texts } = useCourseView();\n const track = useCourseTrack();\n const test = useAsyncResource<Test>(\n () => dataSource.getTest(testId),\n [dataSource, testId],\n );\n const history = useAsyncResource<TestSubmissionList | undefined>(\n () => dataSource.listSubmissions(testId).catch(() => undefined),\n [dataSource, testId],\n );\n // A new form (new shuffle, empty answers) after \"try again\".\n const [formKey, setFormKey] = useState(0);\n\n return (\n <article data-test-id={testId} className={cn(CARD, \"min-w-0 space-y-5\")}>\n <h2 className=\"text-2xl font-semibold break-words\">\n {test.status === \"success\" ? test.data.title : title}\n </h2>\n\n {test.status === \"loading\" && <LoadingBlock />}\n {test.status === \"error\" && (\n <ErrorBox error={test.error} onRetry={test.reload} />\n )}\n {test.status === \"success\" && (\n <>\n {test.data.description.trim() !== \"\" && (\n <p className=\"text-sm break-words whitespace-pre-wrap text-neutral-700 dark:text-neutral-300\">\n {test.data.description}\n </p>\n )}\n <ul className=\"space-y-1 text-sm text-neutral-600 dark:text-neutral-400\">\n {test.data.gating && <li>{texts.testGating}</li>}\n <li>\n {fillTemplate(texts.testPassThreshold, {\n percent: test.data.passThresholdPercent,\n })}\n </li>\n <li>\n {test.data.attemptsLeft == null\n ? texts.testAttemptsUnlimited\n : fillTemplate(texts.testAttemptsLeft, {\n count: test.data.attemptsLeft,\n })}\n </li>\n {test.data.attemptCount > 0 && (\n <li>\n {fillTemplate(texts.testBestScore, {\n percent: test.data.bestScorePercent,\n })}\n </li>\n )}\n </ul>\n {test.data.attemptsLeft != null && test.data.attemptsLeft <= 0 && (\n <p role=\"alert\" className=\"text-sm font-medium text-red-700 dark:text-red-400\">\n {texts.testNoAttemptsLeft}\n </p>\n )}\n <TestForm\n key={formKey}\n test={test.data}\n onSubmitted={(result) => {\n track(\"test_submitted\", {\n lesson_id: testId,\n score_bucket: scoreBucket(result.submission.scorePercent),\n });\n onSubmitted(result);\n history.refresh();\n test.refresh();\n }}\n onTryAgain={() => setFormKey((key) => key + 1)}\n />\n {history.status === \"success\" && history.data && (\n <SubmissionHistory list={history.data} />\n )}\n </>\n )}\n </article>\n );\n}\n"],"mappings":"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","names":["ChevronLeft","List","Play","useCallback","useEffect","useMemo","useRef","useState","accentVars","colors","vars","brand","brandDark","foreground","readableForeground","useCallback","createContext","useContext","CourseViewContext","CourseViewProvider","useCourseView","value","useCourseTrack","analytics","preview","useCourseView","useCallback","event","props","CheckCircle2","ClipboardCheck","Lock","PlayCircle","FileText","flattenItems","course","section","item","findItem","itemId","getAdjacentItems","items","index","getAutoplayTarget","next","computeProgress","total","completed","markLessonCompleted","lessonId","sections","progress","resolveInitialItemId","requestedId","fillTemplate","template","values","match","key","isPlaybackComplete","positionSeconds","durationSeconds","resumePosition","positionSeconds","durationSeconds","formatDuration","totalSeconds","seconds","h","m","s","formatBytes","bytes","units","exponent","value","emptyAnswer","config","item","isAnswered","question","answer","countUnanswered","questions","answers","q","moveItem","list","from","to","copy","splitGapTemplate","template","blanks","segments","pattern","last","match","id","describeAnswer","label","entry","segment","jsx","jsxs","ItemIcon","item","Lock","CheckCircle2","ClipboardCheck","PlayCircle","FileText","OutlineItem","selected","variant","onSelect","texts","useCourseView","lockText","fillTemplate","state","cn","formatDuration","CourseOutline","course","selectedId","className","renderItems","items","section","lessons","i","done","createClient","enumDesc","fileDesc","messageDesc","serviceDesc","file_google_protobuf_timestamp","file_public_v1_user_panel_course","UserPanelCourseService","serviceDesc","file_public_v1_user_panel_course","Code","ConnectError","timestampDate","CourseError","code","message","FAILURE_CODES","failureToCode","failure","toCourseError","error","CourseError","connectError","ConnectError","Code","unwrap","response","result","mapItem","item","mapCourse","course","section","mapRef","ref","mapLesson","lesson","a","timestampDate","mapQuestion","question","base","config","c","s","mapTest","test","q","mapSubmission","submission","mapAnswerFromProto","answer","value","mapAnswerToProto","questionId","mapLessonQuestion","createCourseDataSource","client","auth","credentials","call","run","lessonId","input","saved","testId","answers","durationMs","r","list","content","score","comment","rating","createRpcCourseDataSource","options","host","getTransportHost","client","createClient","UserPanelCourseService","getRpcTransport","createCourseDataSource","projectID","panelHost","getPanelConfig","getPanelSession","ChevronLeft","ChevronRight","Download","Paperclip","useEffect","useMemo","useRef","useState","useState","Star","useState","jsx","RatingStars","value","onSelect","disabled","texts","useCourseView","hover","setHover","useState","shown","fillTemplate","n","filled","star","Star","cn","AlertTriangle","jsx","jsxs","ACCENT_BG","ACCENT_BUTTON","cn","CARD","errorMessage","error","texts","CourseError","ErrorBox","onRetry","className","useCourseView","AlertTriangle","Button","LoadingBlock","label","Skeleton","ProgressBar","percent","value","useCallback","useEffect","useRef","useState","useAsyncResource","load","deps","initialData","state","setState","reloadKey","setReloadKey","requestId","loadRef","skipFirst","id","data","error","reload","key","refresh","mutate","update","current","jsx","jsxs","LessonRatingBox","lessonId","initial","dataSource","texts","preview","useCourseView","track","useCourseTrack","rating","setRating","useState","score","setScore","comment","setComment","state","setState","error","setError","save","e","RatingStars","n","fillTemplate","Textarea","Button","ACCENT_BUTTON","errorMessage","formatDate","date","LessonQuestions","questions","useAsyncResource","content","setContent","ask","text","created","list","ErrorBox","question","DOMPurify","SANITIZE_CONFIG","createHtmlSanitizer","purify","sanitize","node","html","sharedSanitizer","sanitizeLessonHtml","Hls","useCallback","useEffect","useRef","useState","jsx","jsxs","canPlayNativeHls","video","VideoPlayer","src","poster","startAt","durationHint","autoPlay","lessonId","onReport","onEnded","onExpired","className","texts","useCourseView","track","useCourseTrack","trackRef","useRef","videoRef","failed","setFailed","useState","attempt","setAttempt","callbacks","lastReported","report","useCallback","reason","positionSeconds","duration","useEffect","hls","retriedNetwork","retriedMedia","timer","start","Hls","_event","data","status","onPlay","onPause","milestones","lastTime","watchedTo","sendMilestones","percent","milestone","newMilestones","onTimeUpdate","now","onVideoEnded","onVideoError","cn","Button","n","jsx","jsxs","LessonPane","lesson","position","courseType","prev","next","autoplay","onAutoplayChange","startPlaying","onNavigate","onProgressSaved","onEnded","onUrlsExpired","dataSource","texts","preview","useCourseView","track","useCourseTrack","completed","setCompleted","useState","completedRef","useRef","lastSavedPosition","lastKnownPosition","saving","setSaving","useEffect","expiresAt","delay","timer","html","useMemo","sanitizeLessonHtml","save","positionSeconds","durationSeconds","markCompleted","saved","handleReport","report","reachedEnd","isPlaybackComplete","error","markComplete","description","hasBody","VideoPlayer","resumePosition","cn","CARD","Button","ACCENT_BUTTON","e","Paperclip","file","formatBytes","Download","ChevronLeft","ChevronRight","LessonRatingBox","LessonQuestions","COURSE_TEXT_TABLES","getCourseTexts","overrides","locale","getLocale","resolveTexts","ArrowDown","ArrowUp","Check","X","useRef","useState","Fragment","jsx","jsxs","McqEditor","question","answer","disabled","onChange","texts","useCourseView","choices","multiple","choice","checked","id","TodolistEditor","step","FillGapEditor","segments","splitGapTemplate","segment","index","Input","e","SortEditor","labels","i","move","from","to","moveItem","Button","ArrowUp","ArrowDown","QuestionEditor","common","SubmissionHistory","list","submission","Check","X","fillTemplate","TestForm","test","onSubmitted","onTryAgain","dataSource","answers","setAnswers","useState","result","setResult","submitting","setSubmitting","error","setError","startedAt","useRef","questions","outOfAttempts","attemptsLeft","unanswered","countUnanswered","answerOf","q","emptyAnswer","submit","complete","submitted","resultByQuestion","r","cn","questionResult","correctText","describeAnswer","current","errorMessage","ACCENT_BUTTON","TestPane","testId","title","track","useCourseTrack","useAsyncResource","history","formKey","setFormKey","CARD","LoadingBlock","ErrorBox","scoreBucket","key","jsx","jsxs","AUTOPLAY_KEY","useAutoplayPreference","value","setValue","useState","useEffect","update","useCallback","next","toRef","item","LockedPane","texts","useCourseView","cn","CARD","fillTemplate","LessonContainer","course","autoplay","onAutoplayChange","startPlaying","onNavigate","onProgressSaved","onEnded","onLocked","dataSource","lesson","useAsyncResource","lockedByServer","CourseError","LoadingBlock","errorMessage","ErrorBox","adjacent","getAdjacentItems","position","flattenItems","entry","LessonPane","CourseView","selectedId","onSelect","onBack","onCourseChange","onCourseRefresh","preview","track","useCourseTrack","rootRef","useRef","contentsOpen","setContentsOpen","setAutoplay","playOnOpen","setPlayOnOpen","currentId","resolveInitialItemId","findItem","open","itemId","play","outline","CourseOutline","content","TestPane","ref","lessonId","saved","current","markLessonCompleted","target","getAutoplayTarget","isVideo","showResume","Button","ChevronLeft","Play","ProgressBar","List","PanelCourse","accessId","accessToken","customDataSource","onItemChange","initialCourse","textOverrides","locale","theme","className","analytics","panel","usePanelConfig","instrument","useInstrument","useMemo","getCourseTexts","createRpcCourseDataSource","internalItemId","setInternalItemId","select","style","accentVars","courseId","viewValue","InstrumentProvider","CourseViewProvider"]}
1
+ {"version":3,"sources":["../src/course/PanelCourse.tsx","../src/course/accent.ts","../src/course/analytics.ts","../src/course/context.tsx","../src/course/CourseOutline.tsx","../src/course/logic.ts","../src/course/rpc.ts","../src/gen/proto/public/v1/user_panel_course_pb.ts","../src/course/datasource.ts","../src/course/types.ts","../src/course/LessonPane.tsx","../src/course/LessonSocial.tsx","../src/course/RatingStars.tsx","../src/course/ui.tsx","../src/course/useAsyncResource.ts","../src/course/sanitize.ts","../src/course/VideoPlayer.tsx","../src/course/texts.ts","../src/course/TestPane.tsx"],"sourcesContent":["import { ChevronLeft, List, Play } from \"lucide-react\";\nimport {\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from \"react\";\nimport {\n InstrumentProvider,\n useInstrument,\n type AnalyticsOption,\n} from \"../analytics/instrument\";\nimport { usePanelConfig } from \"../panel/PanelProvider\";\nimport type { GetThemeOutput } from \"../panel/types\";\nimport { cn } from \"../lib/utils\";\nimport { Button } from \"../ui/button\";\nimport { accentVars } from \"./accent\";\nimport { useCourseTrack } from \"./analytics\";\nimport { CourseViewProvider, useCourseView } from \"./context\";\nimport { CourseOutline } from \"./CourseOutline\";\nimport { createRpcCourseDataSource } from \"./rpc\";\nimport { LessonPane } from \"./LessonPane\";\nimport {\n fillTemplate,\n findItem,\n flattenItems,\n getAdjacentItems,\n getAutoplayTarget,\n markLessonCompleted,\n resolveInitialItemId,\n type FlatItem,\n} from \"./logic\";\nimport { getCourseTexts, type CourseTexts } from \"./texts\";\nimport { TestPane } from \"./TestPane\";\nimport {\n CourseError,\n type Course,\n type CourseDataSource,\n type ItemRef,\n type Lesson,\n type ProgressSaved,\n} from \"./types\";\nimport { CARD, ErrorBox, LoadingBlock, ProgressBar, errorMessage } from \"./ui\";\nimport { useAsyncResource } from \"./useAsyncResource\";\n\nexport type PanelCourseProps = {\n /** `UserProduct.access_id` of the purchased course. Not needed with `dataSource`. */\n accessId?: string;\n /** Panel session token; read from the panel session when omitted. */\n accessToken?: string;\n /**\n * Replaces the default data source (`UserPanelCourseService` through the\n * panel's transport). The creator preview of the dashboard passes its own.\n */\n dataSource?: CourseDataSource;\n /** Creator preview: shows a badge and never saves progress. */\n preview?: boolean;\n /** The open lesson or test. Controlled together with `onItemChange`. */\n itemId?: string;\n onItemChange?: (itemId: string) => void;\n /** Shows a back button. */\n onBack?: () => void;\n /** Course structure to render right away (server rendering, preview). */\n initialCourse?: Course;\n /** Text overrides; Polish and English defaults by `locale`. */\n texts?: Partial<CourseTexts>;\n /** Defaults to the locale of the library (`setLocale`). */\n locale?: string;\n /** Brand colors; the panel theme (`setTheme`) by default. */\n theme?: GetThemeOutput;\n className?: string;\n /**\n * Usage statistics for your Bitsnap dashboard (course opened, lessons\n * started and completed, video progress, tests, ratings). On by default;\n * `false` turns it off, and the creator `preview` never reports. Inside\n * `<Panel>` the panel's setting applies unless this is given. An object\n * lets you forward every event: `{ onEvent }`. Nothing is stored on the\n * visitor's device.\n */\n analytics?: AnalyticsOption;\n};\n\nconst AUTOPLAY_KEY = \"bitsnap-course-autoplay\";\n\nfunction useAutoplayPreference(): [boolean, (value: boolean) => void] {\n const [value, setValue] = useState(false);\n useEffect(() => {\n try {\n setValue(window.localStorage.getItem(AUTOPLAY_KEY) === \"1\");\n } catch {\n // Storage is not available (private mode); keep the default.\n }\n }, []);\n const update = useCallback((next: boolean) => {\n setValue(next);\n try {\n window.localStorage.setItem(AUTOPLAY_KEY, next ? \"1\" : \"0\");\n } catch {\n // Not persisted.\n }\n }, []);\n return [value, update];\n}\n\nfunction toRef(item: FlatItem | undefined): ItemRef | undefined {\n return item == null ? undefined : { id: item.id, kind: item.kind };\n}\n\nfunction LockedPane({ item }: { item: FlatItem }) {\n const { texts } = useCourseView();\n return (\n <div className={cn(CARD, \"space-y-2\")} role=\"status\">\n <h2 className=\"text-2xl font-semibold break-words\">{item.title}</h2>\n <p className=\"text-sm text-neutral-600 dark:text-neutral-300\">\n {item.lockReason\n ? fillTemplate(texts.lockedPage, {\n test: item.lockReason.gatingTestTitle,\n })\n : texts.locked}\n </p>\n </div>\n );\n}\n\ntype LessonContainerProps = {\n course: Course;\n item: FlatItem;\n autoplay: boolean;\n onAutoplayChange: (value: boolean) => void;\n startPlaying: boolean;\n onNavigate: (ref: ItemRef) => void;\n onProgressSaved: (lessonId: string, saved: ProgressSaved) => void;\n onEnded: () => void;\n onLocked: () => void;\n mobileOutline: ReactNode;\n};\n\nfunction LessonContainer({\n course,\n item,\n autoplay,\n onAutoplayChange,\n startPlaying,\n onNavigate,\n onProgressSaved,\n onEnded,\n onLocked,\n mobileOutline,\n}: LessonContainerProps) {\n const { dataSource, texts } = useCourseView();\n const lesson = useAsyncResource<Lesson>(\n () => dataSource.getLesson(item.id),\n [dataSource, item.id],\n );\n\n const lockedByServer =\n lesson.status === \"error\" &&\n lesson.error instanceof CourseError &&\n lesson.error.code === \"item_locked\";\n useEffect(() => {\n // The structure on screen is stale: take the locks from the server again.\n if (lockedByServer) onLocked();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [lockedByServer]);\n\n if (lesson.status === \"loading\") {\n return <LoadingBlock />;\n }\n if (lesson.status === \"error\") {\n return lockedByServer ? (\n <div className={cn(CARD, \"space-y-2\")} role=\"alert\">\n <h2 className=\"text-2xl font-semibold break-words\">{item.title}</h2>\n <p className=\"text-sm\">{errorMessage(lesson.error, texts)}</p>\n </div>\n ) : (\n <ErrorBox error={lesson.error} onRetry={lesson.reload} />\n );\n }\n\n const adjacent = getAdjacentItems(course, item.id);\n const position =\n flattenItems(course)\n .filter((entry) => entry.kind !== \"test\")\n .findIndex((entry) => entry.id === item.id) + 1;\n return (\n <LessonPane\n key={lesson.data.id}\n lesson={lesson.data}\n position={position}\n mobileOutline={mobileOutline}\n nextItem={findItem(\n course,\n (lesson.data.next ?? toRef(adjacent.next))?.id,\n )}\n courseType={course.type}\n prev={lesson.data.prev ?? toRef(adjacent.prev)}\n next={lesson.data.next ?? toRef(adjacent.next)}\n autoplay={autoplay}\n onAutoplayChange={onAutoplayChange}\n startPlaying={startPlaying}\n onNavigate={onNavigate}\n onProgressSaved={onProgressSaved}\n onEnded={onEnded}\n onUrlsExpired={lesson.refresh}\n />\n );\n}\n\nfunction CourseView({\n course,\n selectedId,\n onSelect,\n onBack,\n onCourseChange,\n onCourseRefresh,\n}: {\n course: Course;\n selectedId?: string;\n onSelect: (itemId: string) => void;\n onBack?: () => void;\n onCourseChange: (update: (course: Course) => Course) => void;\n onCourseRefresh: () => void;\n}) {\n const { texts, preview } = useCourseView();\n const track = useCourseTrack();\n const rootRef = useRef<HTMLDivElement>(null);\n const contentsId = useId();\n const [contentsOpen, setContentsOpen] = useState(false);\n const [autoplay, setAutoplay] = useAutoplayPreference();\n const [playOnOpen, setPlayOnOpen] = useState<string>();\n\n const currentId = resolveInitialItemId(course, selectedId);\n const item = findItem(course, currentId);\n\n // Once per opening of the course.\n useEffect(() => {\n track(\"course_opened\", {});\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [course.id]);\n\n const open = (itemId: string, play = false) => {\n setPlayOnOpen(play ? itemId : undefined);\n setContentsOpen(false);\n onSelect(itemId);\n rootRef.current?.scrollIntoView({ block: \"start\", behavior: \"smooth\" });\n };\n\n const outline = (\n <CourseOutline\n course={course}\n selectedId={currentId}\n variant={course.type === \"video\" ? \"playlist\" : \"modules\"}\n onSelect={(entry) => open(entry.id)}\n />\n );\n\n let content;\n if (item == null) {\n content = <p className=\"text-sm text-neutral-500\">{texts.noItem}</p>;\n } else if (item.locked) {\n content = <LockedPane item={item} />;\n } else if (item.kind === \"test\") {\n content = (\n <TestPane\n key={item.id}\n testId={item.id}\n title={item.title}\n onSubmitted={() => onCourseRefresh()}\n />\n );\n } else {\n content = (\n <LessonContainer\n key={item.id}\n course={course}\n item={item}\n autoplay={autoplay}\n onAutoplayChange={setAutoplay}\n startPlaying={playOnOpen === item.id}\n onNavigate={(ref) => open(ref.id)}\n onProgressSaved={(lessonId, saved) =>\n onCourseChange((current) => ({\n ...(saved.completed\n ? markLessonCompleted(current, lessonId)\n : current),\n completedLessons: saved.completedLessons,\n progressPercent: saved.progressPercent,\n }))\n }\n onEnded={() => {\n if (!autoplay) return;\n const target = getAutoplayTarget(course, item.id);\n if (target) open(target.id, true);\n }}\n onLocked={onCourseRefresh}\n mobileOutline={outline}\n />\n );\n }\n\n const isVideo = course.type === \"video\";\n const showMobileContents =\n !isVideo || item == null || item.locked || item.kind === \"test\";\n const showResume =\n course.resumeItemId !== \"\" &&\n course.resumeItemId !== currentId &&\n findItem(course, course.resumeItemId)?.locked === false;\n\n return (\n <div ref={rootRef} className=\"space-y-4 scroll-mt-4\">\n <header className=\"space-y-3\">\n <div className=\"flex flex-wrap items-center gap-2\">\n {onBack && (\n <Button variant=\"ghost\" size=\"sm\" onClick={onBack}>\n <ChevronLeft aria-hidden />\n {texts.back}\n </Button>\n )}\n {preview && (\n <span\n data-course-preview\n className=\"rounded-full bg-amber-100 px-3 py-0.5 text-xs font-semibold text-amber-900 dark:bg-amber-900/40 dark:text-amber-200\"\n >\n {texts.previewBadge}\n </span>\n )}\n </div>\n <div className=\"flex flex-wrap items-end justify-between gap-4\">\n <div className=\"min-w-0 space-y-1\">\n <h1 className=\"text-2xl font-semibold break-words sm:text-3xl\">\n {course.title}\n </h1>\n </div>\n {showResume && (\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => open(course.resumeItemId)}\n >\n <Play aria-hidden />\n {texts.continueCourse}\n </Button>\n )}\n </div>\n <div className=\"max-w-xl space-y-1 md:max-w-none\">\n {course.progressPercent >= 100 && (\n <p\n role=\"status\"\n className=\"font-semibold text-emerald-700 dark:text-emerald-400\"\n >\n {texts.courseCompleted ?? texts.completed}\n </p>\n )}\n <ProgressBar\n percent={course.progressPercent}\n label={fillTemplate(texts.progressPercent, {\n percent: course.progressPercent,\n })}\n />\n <p className=\"text-xs text-neutral-500 dark:text-neutral-400\">\n {fillTemplate(texts.progressLessons, {\n done: course.completedLessons,\n total: course.totalLessons,\n })}\n {\" · \"}\n {fillTemplate(texts.progressPercent, {\n percent: course.progressPercent,\n })}\n </p>\n </div>\n </header>\n\n <div\n className={cn(\n \"grid gap-6\",\n isVideo\n ? \"lg:grid-cols-[minmax(0,1fr)_380px]\"\n : \"lg:grid-cols-[320px_minmax(0,1fr)]\",\n )}\n >\n {showMobileContents && (\n <div className=\"lg:hidden\">\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"min-h-11\"\n aria-controls={contentsId}\n aria-expanded={contentsOpen}\n onClick={() => setContentsOpen((value) => !value)}\n >\n <List aria-hidden />\n {texts.contents}\n </Button>\n </div>\n )}\n\n {showMobileContents && (\n <aside\n id={contentsId}\n className={cn(\n CARD,\n isVideo && \"lg:hidden\",\n \"p-3 sm:p-3 lg:sticky lg:top-4 lg:max-h-[calc(100vh-2rem)] lg:self-start lg:overflow-y-auto\",\n contentsOpen ? \"block\" : \"hidden\",\n !isVideo && \"lg:block\",\n )}\n >\n {outline}\n </aside>\n )}\n\n <div className=\"min-w-0\" data-course-content>\n {content}\n </div>\n\n {isVideo && (\n <aside\n className={cn(\n CARD,\n \"hidden p-3 sm:p-3 lg:block lg:sticky lg:top-4 lg:max-h-[calc(100vh-2rem)] lg:self-start lg:overflow-y-auto\",\n )}\n >\n {outline}\n </aside>\n )}\n </div>\n </div>\n );\n}\n\n/**\n * The course of a customer inside the panel: structure with progress and\n * locks, lessons with video, text and attachments, tests, questions and\n * ratings. Standard courses get a sidebar of modules, video courses a\n * playlist next to the player.\n *\n * Load it lazily from `bitsnap-react/course`. Inside the panel it needs only\n * `accessId` of the purchased course.\n */\nexport function PanelCourse({\n accessId,\n accessToken,\n dataSource: customDataSource,\n preview = false,\n itemId,\n onItemChange,\n onBack,\n initialCourse,\n texts: textOverrides,\n locale,\n theme,\n className,\n analytics,\n}: PanelCourseProps) {\n const panel = usePanelConfig();\n // The creator preview reports nothing, not even the page view of the page it sits on.\n const instrument = useInstrument(panel.projectID, preview ? false : analytics, \"course\");\n const texts = useMemo(\n () => getCourseTexts(textOverrides, locale),\n [textOverrides, locale],\n );\n const dataSource = useMemo(() => {\n if (customDataSource) return customDataSource;\n if (accessId == null || accessId === \"\") {\n throw new Error(\"PanelCourse needs an accessId or a dataSource\");\n }\n return createRpcCourseDataSource({ accessId, accessToken });\n }, [customDataSource, accessId, accessToken]);\n\n const course = useAsyncResource<Course>(\n () => dataSource.getCourse(),\n [dataSource],\n initialCourse,\n );\n\n const [internalItemId, setInternalItemId] = useState<string>();\n // With `onItemChange` the owner navigates, so no `itemId` means the default\n // item, not the one picked last.\n const selectedId = onItemChange ? itemId : (itemId ?? internalItemId);\n const select = useCallback(\n (next: string) => {\n setInternalItemId(next);\n onItemChange?.(next);\n },\n [onItemChange],\n );\n\n const style = accentVars(theme?.colors ?? panel.theme?.colors);\n\n const courseId = course.status === \"success\" ? course.data.id : undefined;\n const viewValue = useMemo(\n () => ({\n dataSource,\n texts,\n preview,\n analytics: { courseId, instrument },\n }),\n [dataSource, texts, preview, courseId, instrument],\n );\n\n return (\n <InstrumentProvider value={instrument}>\n <CourseViewProvider value={viewValue}>\n <div\n data-bitsnap-course\n data-course-type={course.status === \"success\" ? course.data.type : undefined}\n style={style}\n className={cn(\"font-inter w-full max-w-7xl\", className)}\n >\n {course.status === \"loading\" && (\n <div className=\"space-y-4\">\n {onBack && (\n <Button variant=\"ghost\" size=\"sm\" onClick={onBack}>\n <ChevronLeft aria-hidden />\n {texts.back}\n </Button>\n )}\n <p className=\"sr-only\" role=\"status\">\n {texts.loading}\n </p>\n <LoadingBlock />\n </div>\n )}\n {course.status === \"error\" && (\n <div className=\"space-y-4\">\n {onBack && (\n <Button variant=\"ghost\" size=\"sm\" onClick={onBack}>\n <ChevronLeft aria-hidden />\n {texts.back}\n </Button>\n )}\n <ErrorBox error={course.error} onRetry={course.reload} />\n </div>\n )}\n {course.status === \"success\" && (\n <CourseView\n course={course.data}\n selectedId={selectedId}\n onSelect={select}\n onBack={onBack}\n onCourseChange={course.mutate}\n onCourseRefresh={course.refresh}\n />\n )}\n </div>\n </CourseViewProvider>\n </InstrumentProvider>\n );\n}\n\nexport default PanelCourse;\n","import type { CSSProperties } from \"react\";\nimport { readableForeground } from \"../components/returns/color\";\n\n/**\n * CSS variables of the accent (brand) color of the course view. Without a\n * brand color nothing is set and the library's `--primary` applies.\n */\nexport function accentVars(colors?: {\n brand?: string;\n brandDark?: string;\n}): CSSProperties {\n const vars: Record<string, string> = {};\n const brand = colors?.brand?.trim();\n const brandDark = colors?.brandDark?.trim() || brand;\n if (brand) {\n vars[\"--course-accent\"] = brand;\n const foreground = readableForeground(brand);\n if (foreground) vars[\"--course-accent-fg\"] = foreground;\n }\n if (brandDark) {\n vars[\"--course-accent-dark\"] = brandDark;\n const foreground = readableForeground(brandDark);\n if (foreground) vars[\"--course-accent-dark-fg\"] = foreground;\n }\n return vars as CSSProperties;\n}\n","import { useCallback } from \"react\";\nimport type { EventProps } from \"@/src/analytics/contract\";\nimport { useCourseView } from \"./context\";\n\ntype CourseEventName =\n | \"course_opened\"\n | \"lesson_started\"\n | \"lesson_completed\"\n | \"video_progress\"\n | \"test_submitted\"\n | \"lesson_rated\";\n\n/**\n * Reports a course event; `course_id` is added. Nothing is reported in the\n * creator preview, before the course has loaded, or without analytics.\n */\nexport function useCourseTrack() {\n const { analytics, preview } = useCourseView();\n return useCallback(\n <E extends CourseEventName>(\n event: E,\n props: Omit<EventProps<E>, \"course_id\">,\n ) => {\n if (preview || analytics?.courseId == null) {\n return;\n }\n analytics.instrument.track(event, {\n ...props,\n course_id: analytics.courseId,\n } as unknown as EventProps<E>);\n },\n [analytics, preview],\n );\n}\n","import { createContext, useContext } from \"react\";\nimport type { Instrument } from \"@/src/analytics/instrument\";\nimport type { CourseTexts } from \"./texts\";\nimport type { CourseDataSource } from \"./types\";\n\nexport type CourseAnalytics = {\n /** `undefined` until the course has loaded. */\n courseId?: string;\n instrument: Instrument;\n};\n\nexport type CourseViewContextValue = {\n dataSource: CourseDataSource;\n texts: CourseTexts;\n /** Creator preview: nothing the viewer does is saved as progress. */\n preview: boolean;\n /** Usage statistics; none without it. */\n analytics?: CourseAnalytics;\n};\n\nconst CourseViewContext = createContext<CourseViewContextValue | undefined>(\n undefined,\n);\n\nexport const CourseViewProvider = CourseViewContext.Provider;\n\nexport function useCourseView(): CourseViewContextValue {\n const value = useContext(CourseViewContext);\n if (value == null) {\n throw new Error(\"Course components have to be rendered inside PanelCourse\");\n }\n return value;\n}\n","import {\n CheckCircle2,\n ClipboardCheck,\n Lock,\n PlayCircle,\n FileText,\n} from \"lucide-react\";\nimport { useEffect, useId, useRef, useState } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { useCourseView } from \"./context\";\nimport { fillTemplate, formatDuration } from \"./logic\";\nimport type { Course, CourseItem } from \"./types\";\n\ntype CourseOutlineProps = {\n course: Course;\n selectedId?: string;\n onSelect: (item: CourseItem) => void;\n /**\n * `playlist` (video courses) shows the thumbnail and duration of every\n * video; `modules` (standard courses) is a plain list of lessons.\n */\n variant: \"modules\" | \"playlist\";\n className?: string;\n};\n\nfunction ItemIcon({ item }: { item: CourseItem }) {\n if (item.locked) {\n return <Lock className=\"size-4 text-neutral-500\" aria-hidden />;\n }\n if (item.completed) {\n return (\n <CheckCircle2\n className=\"size-4 text-emerald-600 dark:text-emerald-400\"\n aria-hidden\n />\n );\n }\n if (item.kind === \"test\") {\n return <ClipboardCheck className=\"size-4 text-neutral-500\" aria-hidden />;\n }\n return item.durationSeconds > 0 ? (\n <PlayCircle className=\"size-4 text-neutral-500\" aria-hidden />\n ) : (\n <FileText className=\"size-4 text-neutral-500\" aria-hidden />\n );\n}\n\nfunction OutlineItem({\n item,\n selected,\n variant,\n onSelect,\n}: {\n item: CourseItem;\n selected: boolean;\n variant: \"modules\" | \"playlist\";\n onSelect: (item: CourseItem) => void;\n}) {\n const { texts } = useCourseView();\n const lockText = item.lockReason\n ? fillTemplate(texts.lockedByTest, {\n test: item.lockReason.gatingTestTitle,\n })\n : texts.locked;\n const state = item.locked\n ? lockText\n : item.completed\n ? texts.completed\n : undefined;\n\n return (\n <li>\n <button\n type=\"button\"\n data-item-id={item.id}\n data-locked={item.locked || undefined}\n aria-current={selected ? \"true\" : undefined}\n aria-disabled={item.locked || undefined}\n title={item.locked ? lockText : undefined}\n onClick={() => {\n if (!item.locked) onSelect(item);\n }}\n className={cn(\n \"flex min-h-11 w-full items-start gap-3 rounded-lg p-2 text-left text-sm outline-none transition focus-visible:ring-[3px] focus-visible:ring-ring/50\",\n selected\n ? \"bg-neutral-200 dark:bg-neutral-700\"\n : \"hover:bg-neutral-100 dark:hover:bg-neutral-800\",\n item.locked &&\n \"cursor-not-allowed opacity-60 hover:bg-transparent dark:hover:bg-transparent\",\n )}\n >\n {variant === \"playlist\" && item.kind === \"lesson\" && (\n <span className=\"relative aspect-video w-24 shrink-0 overflow-hidden rounded bg-neutral-200 dark:bg-neutral-800\">\n {item.thumbnailUrl && (\n <img\n src={item.thumbnailUrl}\n alt=\"\"\n loading=\"lazy\"\n draggable={false}\n className=\"size-full object-cover\"\n />\n )}\n {item.durationSeconds > 0 && (\n <span className=\"absolute right-1 bottom-1 rounded bg-black/75 px-1 text-[10px] text-white\">\n {formatDuration(item.durationSeconds)}\n </span>\n )}\n </span>\n )}\n {(variant === \"modules\" || item.kind === \"test\") && (\n <span className=\"mt-0.5 shrink-0\">\n <ItemIcon item={item} />\n </span>\n )}\n <span className=\"min-w-0 flex-1\">\n <span className=\"block font-medium break-words\">{item.title}</span>\n <span className=\"mt-0.5 flex flex-wrap items-center gap-x-2 text-xs text-neutral-500 dark:text-neutral-400\">\n {item.kind === \"test\" && <span>{texts.testBadge}</span>}\n {item.isFree && !item.locked && <span>{texts.free}</span>}\n {variant === \"modules\" && item.durationSeconds > 0 && (\n <span>{formatDuration(item.durationSeconds)}</span>\n )}\n {state && <span>{state}</span>}\n </span>\n </span>\n {variant === \"playlist\" && item.kind === \"lesson\" && (\n <span className=\"mt-0.5 shrink-0\">\n <ItemIcon item={item} />\n </span>\n )}\n </button>\n </li>\n );\n}\n\n/** Modules / sections with their lessons and tests. */\nexport function CourseOutline({\n course,\n selectedId,\n onSelect,\n variant,\n className,\n}: CourseOutlineProps) {\n const { texts } = useCourseView();\n const outlineId = useId();\n const rootRef = useRef<HTMLElement>(null);\n const currentSection = course.sections.find((section) =>\n section.items.some((item) => item.id === selectedId),\n )?.id;\n const [openSections, setOpenSections] = useState<Set<string>>(\n () => new Set(currentSection ? [currentSection] : []),\n );\n useEffect(() => {\n if (currentSection)\n setOpenSections((current) => new Set([...current, currentSection]));\n }, [currentSection]);\n useEffect(() => {\n const selected =\n rootRef.current?.querySelector<HTMLElement>(\"[aria-current]\");\n if (selected && !selected.closest(\"[hidden]\"))\n selected.scrollIntoView?.({ block: \"nearest\" });\n }, [selectedId, openSections]);\n\n const renderItems = (items: CourseItem[]) => (\n <ul className=\"flex flex-col gap-1\">\n {items.map((item) => (\n <OutlineItem\n key={item.id}\n item={item}\n selected={item.id === selectedId}\n variant={variant}\n onSelect={onSelect}\n />\n ))}\n </ul>\n );\n\n return (\n <nav\n ref={rootRef}\n aria-label={texts.contents}\n className={cn(\"space-y-4\", className)}\n >\n {course.sections.map((section) => {\n const lessons = section.items.filter((i) => i.kind === \"lesson\");\n const done = lessons.filter((i) => i.completed).length;\n return (\n <section key={section.id} data-section-id={section.id}>\n <h3 className=\"mb-1 text-sm font-semibold\">\n <button\n type=\"button\"\n className=\"flex min-h-11 w-full items-center justify-between gap-2 rounded-lg px-2 text-left focus-visible:outline focus-visible:outline-2\"\n aria-expanded={openSections.has(section.id)}\n aria-controls={`${outlineId}-${section.id}`}\n onClick={() =>\n setOpenSections((current) => {\n const next = new Set(current);\n if (next.has(section.id)) next.delete(section.id);\n else next.add(section.id);\n return next;\n })\n }\n >\n <span className=\"break-words\">{section.title}</span>\n {lessons.length > 0 && (\n <span className=\"shrink-0 text-xs font-normal text-neutral-500 dark:text-neutral-400\">\n {done}/{lessons.length}\n </span>\n )}\n </button>\n </h3>\n <div\n id={`${outlineId}-${section.id}`}\n hidden={!openSections.has(section.id)}\n >\n {renderItems(section.items)}\n </div>\n </section>\n );\n })}\n {course.finalItems.length > 0 && (\n <section data-section-id=\"final\">\n {renderItems(course.finalItems)}\n </section>\n )}\n </nav>\n );\n}\n","import type {\n Course,\n CourseItem,\n QuestionAnswer,\n TestAnswers,\n TestQuestion,\n TestQuestionConfig,\n} from \"./types\";\n\n/** Playback share from which a lesson counts as watched. */\nexport const COMPLETE_AT_FRACTION = 0.8;\n\n/** How often the player reports the position while it plays. */\nexport const PROGRESS_SAVE_INTERVAL_MS = 15_000;\n\n/** An item together with the section it sits in (empty for final tests). */\nexport type FlatItem = CourseItem & { sectionId: string };\n\n/** All items of the course in the order they are taken. */\nexport function flattenItems(course: Course): FlatItem[] {\n return [\n ...course.sections.flatMap((section) =>\n section.items.map((item) => ({ ...item, sectionId: section.id })),\n ),\n ...course.finalItems.map((item) => ({ ...item, sectionId: \"\" })),\n ];\n}\n\nexport function findItem(\n course: Course,\n itemId: string | undefined,\n): FlatItem | undefined {\n if (itemId == null || itemId === \"\") {\n return undefined;\n }\n return flattenItems(course).find((item) => item.id === itemId);\n}\n\nexport function getAdjacentItems(\n course: Course,\n itemId: string,\n): { prev?: FlatItem; next?: FlatItem } {\n const items = flattenItems(course);\n const index = items.findIndex((item) => item.id === itemId);\n if (index < 0) {\n return {};\n }\n return { prev: items[index - 1], next: items[index + 1] };\n}\n\n/**\n * The item to start autoplay with after `itemId` ended: the next item when it\n * is an unlocked lesson. A test or a locked lesson stops the playlist.\n */\nexport function getAutoplayTarget(\n course: Course,\n itemId: string,\n): FlatItem | undefined {\n const { next } = getAdjacentItems(course, itemId);\n return next != null && next.kind === \"lesson\" && !next.locked\n ? next\n : undefined;\n}\n\n/** Progress counted from the lessons of the structure (tests do not count). */\nexport function computeProgress(course: Pick<Course, \"sections\">): {\n total: number;\n completed: number;\n percent: number;\n} {\n let total = 0;\n let completed = 0;\n for (const section of course.sections) {\n for (const item of section.items) {\n if (item.kind !== \"lesson\") {\n continue;\n }\n total += 1;\n if (item.completed) {\n completed += 1;\n }\n }\n }\n return {\n total,\n completed,\n percent: total === 0 ? 0 : Math.round((completed / total) * 100),\n };\n}\n\n/** The course after a lesson was completed, progress recomputed. */\nexport function markLessonCompleted(course: Course, lessonId: string): Course {\n const sections = course.sections.map((section) => ({\n ...section,\n items: section.items.map((item) =>\n item.id === lessonId && item.kind === \"lesson\"\n ? { ...item, completed: true }\n : item,\n ),\n }));\n const progress = computeProgress({ sections });\n return {\n ...course,\n sections,\n totalLessons: progress.total,\n completedLessons: progress.completed,\n progressPercent: progress.percent,\n };\n}\n\n/**\n * Item to show first: the requested one when it exists, else the one to\n * resume with, else the first unlocked item.\n */\nexport function resolveInitialItemId(\n course: Course,\n requestedId?: string,\n): string | undefined {\n if (findItem(course, requestedId) != null) {\n return requestedId;\n }\n if (findItem(course, course.resumeItemId)?.locked === false) {\n return course.resumeItemId;\n }\n return flattenItems(course).find((item) => !item.locked)?.id;\n}\n\n/** `{name}` placeholders of a text. Unknown placeholders stay untouched. */\nexport function fillTemplate(\n template: string,\n values: Record<string, string | number>,\n): string {\n return template.replace(/\\{(\\w+)\\}/g, (match, key: string) =>\n key in values ? String(values[key]) : match,\n );\n}\n\nexport function isPlaybackComplete(\n positionSeconds: number,\n durationSeconds: number,\n): boolean {\n return (\n durationSeconds > 0 &&\n positionSeconds / durationSeconds >= COMPLETE_AT_FRACTION\n );\n}\n\n/** Whether a periodic save is due. */\nexport function isSaveDue(\n lastSavedAt: number | undefined,\n now: number,\n intervalMs: number = PROGRESS_SAVE_INTERVAL_MS,\n): boolean {\n return lastSavedAt == null || now - lastSavedAt >= intervalMs;\n}\n\n/** Where to resume: nothing when the video was (nearly) watched to the end. */\nexport function resumePosition(\n positionSeconds: number,\n durationSeconds: number,\n): number {\n if (positionSeconds < 5) {\n return 0;\n }\n if (durationSeconds > 0 && positionSeconds >= durationSeconds - 5) {\n return 0;\n }\n return Math.floor(positionSeconds);\n}\n\n/** `m:ss`, or `h:mm:ss` from an hour on. */\nexport function formatDuration(totalSeconds: number): string {\n const seconds = Math.max(0, Math.floor(totalSeconds));\n const h = Math.floor(seconds / 3600);\n const m = Math.floor((seconds % 3600) / 60);\n const s = String(seconds % 60).padStart(2, \"0\");\n return h > 0 ? `${h}:${String(m).padStart(2, \"0\")}:${s}` : `${m}:${s}`;\n}\n\nexport function formatBytes(bytes: number): string {\n if (!(bytes > 0)) {\n return \"0 B\";\n }\n const units = [\"B\", \"KB\", \"MB\", \"GB\"];\n const exponent = Math.min(\n Math.floor(Math.log(bytes) / Math.log(1024)),\n units.length - 1,\n );\n const value = bytes / 1024 ** exponent;\n return `${exponent === 0 || value >= 10 ? Math.round(value) : value.toFixed(1)} ${units[exponent]}`;\n}\n\n// Tests\n\n/** The empty answer of a question, with the order of sort items as given. */\nexport function emptyAnswer(config: TestQuestionConfig): QuestionAnswer {\n switch (config.type) {\n case \"mcq\":\n return { type: \"mcq\", selectedIds: [] };\n case \"todolist\":\n return { type: \"todolist\", checkedIds: [] };\n case \"fill_gap\":\n return { type: \"fill_gap\", values: {} };\n case \"sort_order\":\n return {\n type: \"sort_order\",\n orderedIds: config.items.map((item) => item.id),\n };\n }\n}\n\n/** Whether the customer did anything with the question. */\nexport function isAnswered(\n question: TestQuestion,\n answer: QuestionAnswer | undefined,\n): boolean {\n if (answer == null || answer.type !== question.config.type) {\n return false;\n }\n switch (answer.type) {\n case \"mcq\":\n return answer.selectedIds.length > 0;\n case \"todolist\":\n return answer.checkedIds.length > 0;\n case \"fill_gap\":\n return Object.values(answer.values).some((value) => value.trim() !== \"\");\n case \"sort_order\":\n // The view stores a sort answer once the customer moved an item.\n return answer.orderedIds.length > 0;\n }\n}\n\nexport function countUnanswered(\n questions: TestQuestion[],\n answers: TestAnswers,\n): number {\n return questions.filter((q) => !isAnswered(q, answers[q.id])).length;\n}\n\n/** Moves the element at `from` to `to`; out of range moves change nothing. */\nexport function moveItem<T>(list: readonly T[], from: number, to: number): T[] {\n if (\n from === to ||\n from < 0 ||\n to < 0 ||\n from >= list.length ||\n to >= list.length\n ) {\n return [...list];\n }\n const copy = [...list];\n const [item] = copy.splice(from, 1);\n copy.splice(to, 0, item!);\n return copy;\n}\n\nexport type GapSegment =\n | { type: \"text\"; text: string }\n | { type: \"blank\"; id: string };\n\n/**\n * Splits a fill-in-the-gap template (`Paris is in {{g1}}`) into text and\n * blanks. Placeholders that are not in `blanks` stay text.\n */\nexport function splitGapTemplate(\n template: string,\n blanks: readonly string[],\n): GapSegment[] {\n const segments: GapSegment[] = [];\n const pattern = /\\{\\{\\s*([^{}\\s]+)\\s*\\}\\}/g;\n let last = 0;\n for (const match of template.matchAll(pattern)) {\n const id = match[1]!;\n if (!blanks.includes(id)) {\n continue;\n }\n if (match.index > last) {\n segments.push({ type: \"text\", text: template.slice(last, match.index) });\n }\n segments.push({ type: \"blank\", id });\n last = match.index + match[0].length;\n }\n if (last < template.length) {\n segments.push({ type: \"text\", text: template.slice(last) });\n }\n return segments;\n}\n\n/** A readable form of an answer, used on the result screen. */\nexport function describeAnswer(\n question: TestQuestion,\n answer: QuestionAnswer | undefined,\n): string {\n const { config } = question;\n if (answer == null || answer.type !== config.type) {\n return \"\";\n }\n const label = (list: { id: string; label: string }[], id: string) =>\n list.find((entry) => entry.id === id)?.label ?? id;\n\n if (config.type === \"mcq\" && answer.type === \"mcq\") {\n return answer.selectedIds.map((id) => label(config.choices, id)).join(\", \");\n }\n if (config.type === \"todolist\" && answer.type === \"todolist\") {\n return answer.checkedIds.map((id) => label(config.steps, id)).join(\", \");\n }\n if (config.type === \"sort_order\" && answer.type === \"sort_order\") {\n return answer.orderedIds.map((id) => label(config.items, id)).join(\" → \");\n }\n if (config.type === \"fill_gap\" && answer.type === \"fill_gap\") {\n return splitGapTemplate(config.template, config.blanks)\n .map((segment) =>\n segment.type === \"text\"\n ? segment.text\n : `[${answer.values[segment.id] ?? \"\"}]`,\n )\n .join(\"\");\n }\n return \"\";\n}\n","import { createClient } from \"@connectrpc/connect\";\nimport { getPanelConfig } from \"../panel/config\";\nimport { getPanelSession } from \"../panel/session-storage\";\nimport { getRpcTransport, getTransportHost } from \"../rpc-provider\";\nimport { UserPanelCourseService } from \"../gen/proto/public/v1/user_panel_course_pb\";\nimport { createCourseDataSource } from \"./datasource\";\nimport type { CourseDataSource } from \"./types\";\n\nexport type RpcCourseDataSourceOptions = {\n /** `UserProduct.access_id` of the purchased course. */\n accessId: string;\n /**\n * Panel session token. By default it is read from the panel session of the\n * configured project (`setProjectID`, `setHost`) on every call.\n */\n accessToken?: string;\n /** Bitsnap host; the panel host (`setHost`, bitsnap.pl by default) otherwise. */\n host?: string;\n};\n\n/**\n * Default data source: `UserPanelCourseService` on `{host}/api/rpc`, using the\n * transport and the session token of the panel.\n */\nexport function createRpcCourseDataSource(\n options: RpcCourseDataSourceOptions,\n): CourseDataSource {\n const host = options.host ?? getTransportHost();\n const client = createClient(UserPanelCourseService, getRpcTransport(host));\n return createCourseDataSource(client, {\n accessId: options.accessId,\n accessToken:\n options.accessToken ??\n (() => {\n const { projectID, host: panelHost } = getPanelConfig();\n return getPanelSession(projectID, panelHost);\n }),\n });\n}\n","// @generated by protoc-gen-es v2.10.0 with parameter \"target=ts,ts_nocheck=true\"\n// @generated from file public/v1/user_panel_course.proto (package public_api.v1, syntax proto3)\n/* eslint-disable */\n// @ts-nocheck\n\nimport type { GenEnum, GenFile, GenMessage, GenService } from \"@bufbuild/protobuf/codegenv2\";\nimport { enumDesc, fileDesc, messageDesc, serviceDesc } from \"@bufbuild/protobuf/codegenv2\";\nimport type { Timestamp } from \"@bufbuild/protobuf/wkt\";\nimport { file_google_protobuf_timestamp } from \"@bufbuild/protobuf/wkt\";\nimport type { Message } from \"@bufbuild/protobuf\";\n\n/**\n * Describes the file public/v1/user_panel_course.proto.\n */\nexport const file_public_v1_user_panel_course: GenFile = /*@__PURE__*/\n fileDesc(\"CiFwdWJsaWMvdjEvdXNlcl9wYW5lbF9jb3Vyc2UucHJvdG8SDXB1YmxpY19hcGkudjEiRAoPUGFuZWxMb2NrUmVhc29uEhYKDmdhdGluZ190ZXN0X2lkGAEgASgJEhkKEWdhdGluZ190ZXN0X3RpdGxlGAIgASgJIrQCCg9QYW5lbENvdXJzZUl0ZW0SCgoCaWQYASABKAkSMAoEa2luZBgCIAEoDjIiLnB1YmxpY19hcGkudjEuUGFuZWxDb3Vyc2VJdGVtS2luZBINCgV0aXRsZRgDIAEoCRIPCgdpc19mcmVlGAQgASgIEg4KBmxvY2tlZBgFIAEoCBI4Cgtsb2NrX3JlYXNvbhgGIAEoCzIeLnB1YmxpY19hcGkudjEuUGFuZWxMb2NrUmVhc29uSACIAQESEQoJY29tcGxldGVkGAcgASgIEhgKEGR1cmF0aW9uX3NlY29uZHMYCCABKAUSGgoNdGh1bWJuYWlsX3VybBgJIAEoCUgBiAEBEg4KBmdhdGluZxgKIAEoCEIOCgxfbG9ja19yZWFzb25CEAoOX3RodW1ibmFpbF91cmwicwoSUGFuZWxDb3Vyc2VTZWN0aW9uEgoKAmlkGAEgASgJEg0KBXRpdGxlGAIgASgJEhMKC2Rlc2NyaXB0aW9uGAMgASgJEi0KBWl0ZW1zGAQgAygLMh4ucHVibGljX2FwaS52MS5QYW5lbENvdXJzZUl0ZW0i3wIKC1BhbmVsQ291cnNlEgoKAmlkGAEgASgJEiwKBHR5cGUYAiABKA4yHi5wdWJsaWNfYXBpLnYxLlBhbmVsQ291cnNlVHlwZRINCgV0aXRsZRgDIAEoCRITCgtkZXNjcmlwdGlvbhgEIAEoCRIWCgljb3Zlcl91cmwYBSABKAlIAIgBARIVCg10b3RhbF9sZXNzb25zGAYgASgFEhkKEWNvbXBsZXRlZF9sZXNzb25zGAcgASgFEhgKEHByb2dyZXNzX3BlcmNlbnQYCCABKAUSMwoIc2VjdGlvbnMYCSADKAsyIS5wdWJsaWNfYXBpLnYxLlBhbmVsQ291cnNlU2VjdGlvbhIzCgtmaW5hbF9pdGVtcxgKIAMoCzIeLnB1YmxpY19hcGkudjEuUGFuZWxDb3Vyc2VJdGVtEhYKDnJlc3VtZV9pdGVtX2lkGAsgASgJQgwKCl9jb3Zlcl91cmwiRAoZVXNlclBhbmVsR2V0Q291cnNlUmVxdWVzdBIUCgxhY2Nlc3NfdG9rZW4YASABKAkSEQoJYWNjZXNzX2lkGAIgASgJIosBChpVc2VyUGFuZWxHZXRDb3Vyc2VSZXNwb25zZRItCgdzdWNjZXNzGAEgASgLMhoucHVibGljX2FwaS52MS5QYW5lbENvdXJzZUgAEjQKB2ZhaWx1cmUYAiABKA4yIS5wdWJsaWNfYXBpLnYxLlBhbmVsQ291cnNlRmFpbHVyZUgAQggKBnJlc3VsdCJfCgpQYW5lbFZpZGVvEg8KB2hsc191cmwYASABKAkSFwoKcG9zdGVyX3VybBgCIAEoCUgAiAEBEhgKEGR1cmF0aW9uX3NlY29uZHMYAyABKAVCDQoLX3Bvc3Rlcl91cmwiRgoPUGFuZWxBdHRhY2htZW50EgoKAmlkGAEgASgJEgwKBG5hbWUYAiABKAkSDAoEc2l6ZRgDIAEoAxILCgN1cmwYBCABKAkiTAoMUGFuZWxJdGVtUmVmEgoKAmlkGAEgASgJEjAKBGtpbmQYAiABKA4yIi5wdWJsaWNfYXBpLnYxLlBhbmVsQ291cnNlSXRlbUtpbmQigQQKC1BhbmVsTGVzc29uEgoKAmlkGAEgASgJEhIKCnNlY3Rpb25faWQYAiABKAkSDQoFdGl0bGUYAyABKAkSEwoLZGVzY3JpcHRpb24YBCABKAkSFAoMY29udGVudF9odG1sGAUgASgJEi0KBXZpZGVvGAYgASgLMhkucHVibGljX2FwaS52MS5QYW5lbFZpZGVvSACIAQESMwoLYXR0YWNobWVudHMYByADKAsyHi5wdWJsaWNfYXBpLnYxLlBhbmVsQXR0YWNobWVudBIuCgRwcmV2GAggASgLMhsucHVibGljX2FwaS52MS5QYW5lbEl0ZW1SZWZIAYgBARIuCgRuZXh0GAkgASgLMhsucHVibGljX2FwaS52MS5QYW5lbEl0ZW1SZWZIAogBARIRCgljb21wbGV0ZWQYCiABKAgSGAoQcG9zaXRpb25fc2Vjb25kcxgLIAEoBRIRCglteV9yYXRpbmcYDCABKAUSFgoOYXZlcmFnZV9yYXRpbmcYDSABKAISFAoMcmF0aW5nX2NvdW50GA4gASgFEjcKDnVybHNfZXhwaXJlX2F0GA8gASgLMhouZ29vZ2xlLnByb3RvYnVmLlRpbWVzdGFtcEgDiAEBQggKBl92aWRlb0IHCgVfcHJldkIHCgVfbmV4dEIRCg9fdXJsc19leHBpcmVfYXQiVwoZVXNlclBhbmVsR2V0TGVzc29uUmVxdWVzdBIUCgxhY2Nlc3NfdG9rZW4YASABKAkSEQoJYWNjZXNzX2lkGAIgASgJEhEKCWxlc3Nvbl9pZBgDIAEoCSKLAQoaVXNlclBhbmVsR2V0TGVzc29uUmVzcG9uc2USLQoHc3VjY2VzcxgBIAEoCzIaLnB1YmxpY19hcGkudjEuUGFuZWxMZXNzb25IABI0CgdmYWlsdXJlGAIgASgOMiEucHVibGljX2FwaS52MS5QYW5lbENvdXJzZUZhaWx1cmVIAEIICgZyZXN1bHQipwEKIlVzZXJQYW5lbFNhdmVMZXNzb25Qcm9ncmVzc1JlcXVlc3QSFAoMYWNjZXNzX3Rva2VuGAEgASgJEhEKCWFjY2Vzc19pZBgCIAEoCRIRCglsZXNzb25faWQYAyABKAkSEQoJY29tcGxldGVkGAQgASgIEhgKEHBvc2l0aW9uX3NlY29uZHMYBSABKAUSGAoQZHVyYXRpb25fc2Vjb25kcxgGIAEoBSKFAgojVXNlclBhbmVsU2F2ZUxlc3NvblByb2dyZXNzUmVzcG9uc2USTAoHc3VjY2VzcxgBIAEoCzI5LnB1YmxpY19hcGkudjEuVXNlclBhbmVsU2F2ZUxlc3NvblByb2dyZXNzUmVzcG9uc2UuUmVzdWx0SAASNAoHZmFpbHVyZRgCIAEoDjIhLnB1YmxpY19hcGkudjEuUGFuZWxDb3Vyc2VGYWlsdXJlSAAaUAoGUmVzdWx0EhEKCWNvbXBsZXRlZBgBIAEoCBIZChFjb21wbGV0ZWRfbGVzc29ucxgCIAEoBRIYChBwcm9ncmVzc19wZXJjZW50GAMgASgFQggKBnJlc3VsdCIoCgtQYW5lbENob2ljZRIKCgJpZBgBIAEoCRINCgVsYWJlbBgCIAEoCSJPCg5QYW5lbE1jcUNvbmZpZxIrCgdjaG9pY2VzGAEgAygLMhoucHVibGljX2FwaS52MS5QYW5lbENob2ljZRIQCghtdWx0aXBsZRgCIAEoCCKCAQoTUGFuZWxUb2RvbGlzdENvbmZpZxI2CgVzdGVwcxgBIAMoCzInLnB1YmxpY19hcGkudjEuUGFuZWxUb2RvbGlzdENvbmZpZy5TdGVwGjMKBFN0ZXASCgoCaWQYASABKAkSDQoFbGFiZWwYAiABKAkSEAoIcmVxdWlyZWQYAyABKAgiNgoSUGFuZWxGaWxsR2FwQ29uZmlnEhAKCHRlbXBsYXRlGAEgASgJEg4KBmJsYW5rcxgCIAMoCSJBChRQYW5lbFNvcnRPcmRlckNvbmZpZxIpCgVpdGVtcxgBIAMoCzIaLnB1YmxpY19hcGkudjEuUGFuZWxDaG9pY2UioQIKEVBhbmVsVGVzdFF1ZXN0aW9uEgoKAmlkGAEgASgJEg4KBnByb21wdBgCIAEoCRIOCgZwb2ludHMYAyABKAUSLAoDbWNxGAogASgLMh0ucHVibGljX2FwaS52MS5QYW5lbE1jcUNvbmZpZ0gAEjYKCHRvZG9saXN0GAsgASgLMiIucHVibGljX2FwaS52MS5QYW5lbFRvZG9saXN0Q29uZmlnSAASNQoIZmlsbF9nYXAYDCABKAsyIS5wdWJsaWNfYXBpLnYxLlBhbmVsRmlsbEdhcENvbmZpZ0gAEjkKCnNvcnRfb3JkZXIYDSABKAsyIy5wdWJsaWNfYXBpLnYxLlBhbmVsU29ydE9yZGVyQ29uZmlnSABCCAoGY29uZmlnIqsCCglQYW5lbFRlc3QSCgoCaWQYASABKAkSDQoFdGl0bGUYAiABKAkSEwoLZGVzY3JpcHRpb24YAyABKAkSDgoGZ2F0aW5nGAQgASgIEh4KFnBhc3NfdGhyZXNob2xkX3BlcmNlbnQYBSABKAUSGAoQYXR0ZW1wdHNfYWxsb3dlZBgGIAEoBRIaCg1hdHRlbXB0c19sZWZ0GAcgASgFSACIAQESFQoNYXR0ZW1wdF9jb3VudBgIIAEoBRIaChJiZXN0X3Njb3JlX3BlcmNlbnQYCSABKAUSDgoGcGFzc2VkGAogASgIEjMKCXF1ZXN0aW9ucxgLIAMoCzIgLnB1YmxpY19hcGkudjEuUGFuZWxUZXN0UXVlc3Rpb25CEAoOX2F0dGVtcHRzX2xlZnQiUwoXVXNlclBhbmVsR2V0VGVzdFJlcXVlc3QSFAoMYWNjZXNzX3Rva2VuGAEgASgJEhEKCWFjY2Vzc19pZBgCIAEoCRIPCgd0ZXN0X2lkGAMgASgJIocBChhVc2VyUGFuZWxHZXRUZXN0UmVzcG9uc2USKwoHc3VjY2VzcxgBIAEoCzIYLnB1YmxpY19hcGkudjEuUGFuZWxUZXN0SAASNAoHZmFpbHVyZRgCIAEoDjIhLnB1YmxpY19hcGkudjEuUGFuZWxDb3Vyc2VGYWlsdXJlSABCCAoGcmVzdWx0IiYKDlBhbmVsTWNxQW5zd2VyEhQKDHNlbGVjdGVkX2lkcxgBIAMoCSIqChNQYW5lbFRvZG9saXN0QW5zd2VyEhMKC2NoZWNrZWRfaWRzGAEgAygJIoIBChJQYW5lbEZpbGxHYXBBbnN3ZXISPQoGdmFsdWVzGAEgAygLMi0ucHVibGljX2FwaS52MS5QYW5lbEZpbGxHYXBBbnN3ZXIuVmFsdWVzRW50cnkaLQoLVmFsdWVzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASIrChRQYW5lbFNvcnRPcmRlckFuc3dlchITCgtvcmRlcmVkX2lkcxgBIAMoCSKDAgoLUGFuZWxBbnN3ZXISEwoLcXVlc3Rpb25faWQYASABKAkSLAoDbWNxGAogASgLMh0ucHVibGljX2FwaS52MS5QYW5lbE1jcUFuc3dlckgAEjYKCHRvZG9saXN0GAsgASgLMiIucHVibGljX2FwaS52MS5QYW5lbFRvZG9saXN0QW5zd2VySAASNQoIZmlsbF9nYXAYDCABKAsyIS5wdWJsaWNfYXBpLnYxLlBhbmVsRmlsbEdhcEFuc3dlckgAEjkKCnNvcnRfb3JkZXIYDSABKAsyIy5wdWJsaWNfYXBpLnYxLlBhbmVsU29ydE9yZGVyQW5zd2VySABCBwoFdmFsdWUijQEKE1BhbmVsVGVzdFN1Ym1pc3Npb24SCgoCaWQYASABKAkSFQoNc2NvcmVfcGVyY2VudBgCIAEoBRIOCgZwYXNzZWQYAyABKAgSEwoLZHVyYXRpb25fbXMYBCABKAMSLgoKY3JlYXRlZF9hdBgFIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAilwEKE1BhbmVsUXVlc3Rpb25SZXN1bHQSEwoLcXVlc3Rpb25faWQYASABKAkSDwoHY29ycmVjdBgCIAEoCBIOCgZwb2ludHMYAyABKAUSNwoOY29ycmVjdF9hbnN3ZXIYBCABKAsyGi5wdWJsaWNfYXBpLnYxLlBhbmVsQW5zd2VySACIAQFCEQoPX2NvcnJlY3RfYW5zd2VyIpgBChpVc2VyUGFuZWxTdWJtaXRUZXN0UmVxdWVzdBIUCgxhY2Nlc3NfdG9rZW4YASABKAkSEQoJYWNjZXNzX2lkGAIgASgJEg8KB3Rlc3RfaWQYAyABKAkSKwoHYW5zd2VycxgEIAMoCzIaLnB1YmxpY19hcGkudjEuUGFuZWxBbnN3ZXISEwoLZHVyYXRpb25fbXMYBSABKAMiyQIKG1VzZXJQYW5lbFN1Ym1pdFRlc3RSZXNwb25zZRJECgdzdWNjZXNzGAEgASgLMjEucHVibGljX2FwaS52MS5Vc2VyUGFuZWxTdWJtaXRUZXN0UmVzcG9uc2UuUmVzdWx0SAASNAoHZmFpbHVyZRgCIAEoDjIhLnB1YmxpY19hcGkudjEuUGFuZWxDb3Vyc2VGYWlsdXJlSAAaowEKBlJlc3VsdBI2CgpzdWJtaXNzaW9uGAEgASgLMiIucHVibGljX2FwaS52MS5QYW5lbFRlc3RTdWJtaXNzaW9uEjMKB3Jlc3VsdHMYAiADKAsyIi5wdWJsaWNfYXBpLnYxLlBhbmVsUXVlc3Rpb25SZXN1bHQSGgoNYXR0ZW1wdHNfbGVmdBgDIAEoBUgAiAEBQhAKDl9hdHRlbXB0c19sZWZ0QggKBnJlc3VsdCJfCiNVc2VyUGFuZWxMaXN0VGVzdFN1Ym1pc3Npb25zUmVxdWVzdBIUCgxhY2Nlc3NfdG9rZW4YASABKAkSEQoJYWNjZXNzX2lkGAIgASgJEg8KB3Rlc3RfaWQYAyABKAkiqwIKJFVzZXJQYW5lbExpc3RUZXN0U3VibWlzc2lvbnNSZXNwb25zZRJNCgdzdWNjZXNzGAEgASgLMjoucHVibGljX2FwaS52MS5Vc2VyUGFuZWxMaXN0VGVzdFN1Ym1pc3Npb25zUmVzcG9uc2UuUmVzdWx0SAASNAoHZmFpbHVyZRgCIAEoDjIhLnB1YmxpY19hcGkudjEuUGFuZWxDb3Vyc2VGYWlsdXJlSAAadAoGUmVzdWx0EjcKC3N1Ym1pc3Npb25zGAEgAygLMiIucHVibGljX2FwaS52MS5QYW5lbFRlc3RTdWJtaXNzaW9uEhoKEmJlc3Rfc2NvcmVfcGVyY2VudBgCIAEoBRIVCg1hdHRlbXB0X2NvdW50GAMgASgFQggKBnJlc3VsdCJWChNQYW5lbFF1ZXN0aW9uQW5zd2VyEg8KB2NvbnRlbnQYASABKAkSLgoKY3JlYXRlZF9hdBgCIAEoCzIaLmdvb2dsZS5wcm90b2J1Zi5UaW1lc3RhbXAizAEKE1BhbmVsTGVzc29uUXVlc3Rpb24SCgoCaWQYASABKAkSDwoHY29udGVudBgCIAEoCRITCgthdXRob3JfbmFtZRgDIAEoCRIPCgdpc19taW5lGAQgASgIEi4KCmNyZWF0ZWRfYXQYBSABKAsyGi5nb29nbGUucHJvdG9idWYuVGltZXN0YW1wEjcKBmFuc3dlchgGIAEoCzIiLnB1YmxpY19hcGkudjEuUGFuZWxRdWVzdGlvbkFuc3dlckgAiAEBQgkKB19hbnN3ZXIiYQojVXNlclBhbmVsTGlzdExlc3NvblF1ZXN0aW9uc1JlcXVlc3QSFAoMYWNjZXNzX3Rva2VuGAEgASgJEhEKCWFjY2Vzc19pZBgCIAEoCRIRCglsZXNzb25faWQYAyABKAki9gEKJFVzZXJQYW5lbExpc3RMZXNzb25RdWVzdGlvbnNSZXNwb25zZRJNCgdzdWNjZXNzGAEgASgLMjoucHVibGljX2FwaS52MS5Vc2VyUGFuZWxMaXN0TGVzc29uUXVlc3Rpb25zUmVzcG9uc2UuUmVzdWx0SAASNAoHZmFpbHVyZRgCIAEoDjIhLnB1YmxpY19hcGkudjEuUGFuZWxDb3Vyc2VGYWlsdXJlSAAaPwoGUmVzdWx0EjUKCXF1ZXN0aW9ucxgBIAMoCzIiLnB1YmxpY19hcGkudjEuUGFuZWxMZXNzb25RdWVzdGlvbkIICgZyZXN1bHQicAohVXNlclBhbmVsQXNrTGVzc29uUXVlc3Rpb25SZXF1ZXN0EhQKDGFjY2Vzc190b2tlbhgBIAEoCRIRCglhY2Nlc3NfaWQYAiABKAkSEQoJbGVzc29uX2lkGAMgASgJEg8KB2NvbnRlbnQYBCABKAkimwEKIlVzZXJQYW5lbEFza0xlc3NvblF1ZXN0aW9uUmVzcG9uc2USNQoHc3VjY2VzcxgBIAEoCzIiLnB1YmxpY19hcGkudjEuUGFuZWxMZXNzb25RdWVzdGlvbkgAEjQKB2ZhaWx1cmUYAiABKA4yIS5wdWJsaWNfYXBpLnYxLlBhbmVsQ291cnNlRmFpbHVyZUgAQggKBnJlc3VsdCJ4ChpVc2VyUGFuZWxSYXRlTGVzc29uUmVxdWVzdBIUCgxhY2Nlc3NfdG9rZW4YASABKAkSEQoJYWNjZXNzX2lkGAIgASgJEhEKCWxlc3Nvbl9pZBgDIAEoCRINCgVzY29yZRgEIAEoBRIPCgdjb21tZW50GAUgASgJIu4BChtVc2VyUGFuZWxSYXRlTGVzc29uUmVzcG9uc2USRAoHc3VjY2VzcxgBIAEoCzIxLnB1YmxpY19hcGkudjEuVXNlclBhbmVsUmF0ZUxlc3NvblJlc3BvbnNlLlJlc3VsdEgAEjQKB2ZhaWx1cmUYAiABKA4yIS5wdWJsaWNfYXBpLnYxLlBhbmVsQ291cnNlRmFpbHVyZUgAGkkKBlJlc3VsdBIRCglteV9yYXRpbmcYASABKAUSFgoOYXZlcmFnZV9yYXRpbmcYAiABKAISFAoMcmF0aW5nX2NvdW50GAMgASgFQggKBnJlc3VsdCr6AgoSUGFuZWxDb3Vyc2VGYWlsdXJlEiAKHFBBTkVMX0NPVVJTRV9GQUlMVVJFX1VOS05PV04QABIlCiFQQU5FTF9DT1VSU0VfRkFJTFVSRV9VTkFVVEhPUklaRUQQARIpCiVQQU5FTF9DT1VSU0VfRkFJTFVSRV9DT1VSU0VfTk9UX0ZPVU5EEAISJwojUEFORUxfQ09VUlNFX0ZBSUxVUkVfSVRFTV9OT1RfRk9VTkQQAxIkCiBQQU5FTF9DT1VSU0VfRkFJTFVSRV9JVEVNX0xPQ0tFRBAEEikKJVBBTkVMX0NPVVJTRV9GQUlMVVJFX05PX0FUVEVNUFRTX0xFRlQQBRIpCiVQQU5FTF9DT1VSU0VfRkFJTFVSRV9JTlZBTElEX0FSR1VNRU5UEAYSJQohUEFORUxfQ09VUlNFX0ZBSUxVUkVfUkFURV9MSU1JVEVEEAcSJAogUEFORUxfQ09VUlNFX0ZBSUxVUkVfVU5BVkFJTEFCTEUQCCptCg9QYW5lbENvdXJzZVR5cGUSHQoZUEFORUxfQ09VUlNFX1RZUEVfVU5LTk9XThAAEh4KGlBBTkVMX0NPVVJTRV9UWVBFX1NUQU5EQVJEEAESGwoXUEFORUxfQ09VUlNFX1RZUEVfVklERU8QAip9ChNQYW5lbENvdXJzZUl0ZW1LaW5kEiIKHlBBTkVMX0NPVVJTRV9JVEVNX0tJTkRfVU5LTk9XThAAEiEKHVBBTkVMX0NPVVJTRV9JVEVNX0tJTkRfTEVTU09OEAESHwobUEFORUxfQ09VUlNFX0lURU1fS0lORF9URVNUEAIy+QcKFlVzZXJQYW5lbENvdXJzZVNlcnZpY2USYAoJR2V0Q291cnNlEigucHVibGljX2FwaS52MS5Vc2VyUGFuZWxHZXRDb3Vyc2VSZXF1ZXN0GikucHVibGljX2FwaS52MS5Vc2VyUGFuZWxHZXRDb3Vyc2VSZXNwb25zZRJgCglHZXRMZXNzb24SKC5wdWJsaWNfYXBpLnYxLlVzZXJQYW5lbEdldExlc3NvblJlcXVlc3QaKS5wdWJsaWNfYXBpLnYxLlVzZXJQYW5lbEdldExlc3NvblJlc3BvbnNlEnsKElNhdmVMZXNzb25Qcm9ncmVzcxIxLnB1YmxpY19hcGkudjEuVXNlclBhbmVsU2F2ZUxlc3NvblByb2dyZXNzUmVxdWVzdBoyLnB1YmxpY19hcGkudjEuVXNlclBhbmVsU2F2ZUxlc3NvblByb2dyZXNzUmVzcG9uc2USWgoHR2V0VGVzdBImLnB1YmxpY19hcGkudjEuVXNlclBhbmVsR2V0VGVzdFJlcXVlc3QaJy5wdWJsaWNfYXBpLnYxLlVzZXJQYW5lbEdldFRlc3RSZXNwb25zZRJjCgpTdWJtaXRUZXN0EikucHVibGljX2FwaS52MS5Vc2VyUGFuZWxTdWJtaXRUZXN0UmVxdWVzdBoqLnB1YmxpY19hcGkudjEuVXNlclBhbmVsU3VibWl0VGVzdFJlc3BvbnNlEn4KE0xpc3RUZXN0U3VibWlzc2lvbnMSMi5wdWJsaWNfYXBpLnYxLlVzZXJQYW5lbExpc3RUZXN0U3VibWlzc2lvbnNSZXF1ZXN0GjMucHVibGljX2FwaS52MS5Vc2VyUGFuZWxMaXN0VGVzdFN1Ym1pc3Npb25zUmVzcG9uc2USfgoTTGlzdExlc3NvblF1ZXN0aW9ucxIyLnB1YmxpY19hcGkudjEuVXNlclBhbmVsTGlzdExlc3NvblF1ZXN0aW9uc1JlcXVlc3QaMy5wdWJsaWNfYXBpLnYxLlVzZXJQYW5lbExpc3RMZXNzb25RdWVzdGlvbnNSZXNwb25zZRJ4ChFBc2tMZXNzb25RdWVzdGlvbhIwLnB1YmxpY19hcGkudjEuVXNlclBhbmVsQXNrTGVzc29uUXVlc3Rpb25SZXF1ZXN0GjEucHVibGljX2FwaS52MS5Vc2VyUGFuZWxBc2tMZXNzb25RdWVzdGlvblJlc3BvbnNlEmMKClJhdGVMZXNzb24SKS5wdWJsaWNfYXBpLnYxLlVzZXJQYW5lbFJhdGVMZXNzb25SZXF1ZXN0GioucHVibGljX2FwaS52MS5Vc2VyUGFuZWxSYXRlTGVzc29uUmVzcG9uc2ViBnByb3RvMw\", [file_google_protobuf_timestamp]);\n\n/**\n * Why an item is locked.\n *\n * @generated from message public_api.v1.PanelLockReason\n */\nexport type PanelLockReason = Message<\"public_api.v1.PanelLockReason\"> & {\n /**\n * The test (of an earlier module) that has to be passed first.\n *\n * @generated from field: string gating_test_id = 1;\n */\n gatingTestId: string;\n\n /**\n * @generated from field: string gating_test_title = 2;\n */\n gatingTestTitle: string;\n};\n\n/**\n * Describes the message public_api.v1.PanelLockReason.\n * Use `create(PanelLockReasonSchema)` to create a new message.\n */\nexport const PanelLockReasonSchema: GenMessage<PanelLockReason> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 0);\n\n/**\n * A lesson or a test in the list of a course.\n *\n * @generated from message public_api.v1.PanelCourseItem\n */\nexport type PanelCourseItem = Message<\"public_api.v1.PanelCourseItem\"> & {\n /**\n * Unique among all items of the course, lessons and tests alike.\n *\n * @generated from field: string id = 1;\n */\n id: string;\n\n /**\n * @generated from field: public_api.v1.PanelCourseItemKind kind = 2;\n */\n kind: PanelCourseItemKind;\n\n /**\n * @generated from field: string title = 3;\n */\n title: string;\n\n /**\n * Free for everyone, shown as a badge.\n *\n * @generated from field: bool is_free = 4;\n */\n isFree: boolean;\n\n /**\n * A locked item can't be opened (GetLesson/GetTest fail with ITEM_LOCKED).\n *\n * @generated from field: bool locked = 5;\n */\n locked: boolean;\n\n /**\n * Set when locked.\n *\n * @generated from field: optional public_api.v1.PanelLockReason lock_reason = 6;\n */\n lockReason?: PanelLockReason;\n\n /**\n * A lesson: completed. A test: passed.\n *\n * @generated from field: bool completed = 7;\n */\n completed: boolean;\n\n /**\n * Video length in seconds; 0 when unknown or the lesson has no video.\n *\n * @generated from field: int32 duration_seconds = 8;\n */\n durationSeconds: number;\n\n /**\n * Signed URL of the thumbnail; unset when there is none.\n *\n * @generated from field: optional string thumbnail_url = 9;\n */\n thumbnailUrl?: string;\n\n /**\n * Tests only: lessons of the following sections stay locked until it is passed.\n *\n * @generated from field: bool gating = 10;\n */\n gating: boolean;\n};\n\n/**\n * Describes the message public_api.v1.PanelCourseItem.\n * Use `create(PanelCourseItemSchema)` to create a new message.\n */\nexport const PanelCourseItemSchema: GenMessage<PanelCourseItem> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 1);\n\n/**\n * A module (standard course) or a section (video course).\n *\n * @generated from message public_api.v1.PanelCourseSection\n */\nexport type PanelCourseSection = Message<\"public_api.v1.PanelCourseSection\"> & {\n /**\n * @generated from field: string id = 1;\n */\n id: string;\n\n /**\n * @generated from field: string title = 2;\n */\n title: string;\n\n /**\n * @generated from field: string description = 3;\n */\n description: string;\n\n /**\n * Lessons in order, then the tests of the section.\n *\n * @generated from field: repeated public_api.v1.PanelCourseItem items = 4;\n */\n items: PanelCourseItem[];\n};\n\n/**\n * Describes the message public_api.v1.PanelCourseSection.\n * Use `create(PanelCourseSectionSchema)` to create a new message.\n */\nexport const PanelCourseSectionSchema: GenMessage<PanelCourseSection> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 2);\n\n/**\n * @generated from message public_api.v1.PanelCourse\n */\nexport type PanelCourse = Message<\"public_api.v1.PanelCourse\"> & {\n /**\n * Id of the course in Bitsnap (stable, used in the URL of the panel).\n *\n * @generated from field: string id = 1;\n */\n id: string;\n\n /**\n * @generated from field: public_api.v1.PanelCourseType type = 2;\n */\n type: PanelCourseType;\n\n /**\n * @generated from field: string title = 3;\n */\n title: string;\n\n /**\n * @generated from field: string description = 4;\n */\n description: string;\n\n /**\n * Signed URL of the cover; unset when the course has none.\n *\n * @generated from field: optional string cover_url = 5;\n */\n coverUrl?: string;\n\n /**\n * @generated from field: int32 total_lessons = 6;\n */\n totalLessons: number;\n\n /**\n * @generated from field: int32 completed_lessons = 7;\n */\n completedLessons: number;\n\n /**\n * 0 to 100.\n *\n * @generated from field: int32 progress_percent = 8;\n */\n progressPercent: number;\n\n /**\n * @generated from field: repeated public_api.v1.PanelCourseSection sections = 9;\n */\n sections: PanelCourseSection[];\n\n /**\n * Tests of the whole course, shown after the last section.\n *\n * @generated from field: repeated public_api.v1.PanelCourseItem final_items = 10;\n */\n finalItems: PanelCourseItem[];\n\n /**\n * Item to continue with (first unfinished and unlocked lesson); empty when\n * everything is done.\n *\n * @generated from field: string resume_item_id = 11;\n */\n resumeItemId: string;\n};\n\n/**\n * Describes the message public_api.v1.PanelCourse.\n * Use `create(PanelCourseSchema)` to create a new message.\n */\nexport const PanelCourseSchema: GenMessage<PanelCourse> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 3);\n\n/**\n * @generated from message public_api.v1.UserPanelGetCourseRequest\n */\nexport type UserPanelGetCourseRequest = Message<\"public_api.v1.UserPanelGetCourseRequest\"> & {\n /**\n * @generated from field: string access_token = 1;\n */\n accessToken: string;\n\n /**\n * @generated from field: string access_id = 2;\n */\n accessId: string;\n};\n\n/**\n * Describes the message public_api.v1.UserPanelGetCourseRequest.\n * Use `create(UserPanelGetCourseRequestSchema)` to create a new message.\n */\nexport const UserPanelGetCourseRequestSchema: GenMessage<UserPanelGetCourseRequest> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 4);\n\n/**\n * @generated from message public_api.v1.UserPanelGetCourseResponse\n */\nexport type UserPanelGetCourseResponse = Message<\"public_api.v1.UserPanelGetCourseResponse\"> & {\n /**\n * @generated from oneof public_api.v1.UserPanelGetCourseResponse.result\n */\n result: {\n /**\n * @generated from field: public_api.v1.PanelCourse success = 1;\n */\n value: PanelCourse;\n case: \"success\";\n } | {\n /**\n * @generated from field: public_api.v1.PanelCourseFailure failure = 2;\n */\n value: PanelCourseFailure;\n case: \"failure\";\n } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message public_api.v1.UserPanelGetCourseResponse.\n * Use `create(UserPanelGetCourseResponseSchema)` to create a new message.\n */\nexport const UserPanelGetCourseResponseSchema: GenMessage<UserPanelGetCourseResponse> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 5);\n\n/**\n * @generated from message public_api.v1.PanelVideo\n */\nexport type PanelVideo = Message<\"public_api.v1.PanelVideo\"> & {\n /**\n * Cloudflare Stream HLS manifest URL with a signed token.\n *\n * @generated from field: string hls_url = 1;\n */\n hlsUrl: string;\n\n /**\n * @generated from field: optional string poster_url = 2;\n */\n posterUrl?: string;\n\n /**\n * @generated from field: int32 duration_seconds = 3;\n */\n durationSeconds: number;\n};\n\n/**\n * Describes the message public_api.v1.PanelVideo.\n * Use `create(PanelVideoSchema)` to create a new message.\n */\nexport const PanelVideoSchema: GenMessage<PanelVideo> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 6);\n\n/**\n * @generated from message public_api.v1.PanelAttachment\n */\nexport type PanelAttachment = Message<\"public_api.v1.PanelAttachment\"> & {\n /**\n * @generated from field: string id = 1;\n */\n id: string;\n\n /**\n * @generated from field: string name = 2;\n */\n name: string;\n\n /**\n * Bytes.\n *\n * @generated from field: int64 size = 3;\n */\n size: bigint;\n\n /**\n * Presigned S3 URL.\n *\n * @generated from field: string url = 4;\n */\n url: string;\n};\n\n/**\n * Describes the message public_api.v1.PanelAttachment.\n * Use `create(PanelAttachmentSchema)` to create a new message.\n */\nexport const PanelAttachmentSchema: GenMessage<PanelAttachment> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 7);\n\n/**\n * Neighbour of a lesson in the course order.\n *\n * @generated from message public_api.v1.PanelItemRef\n */\nexport type PanelItemRef = Message<\"public_api.v1.PanelItemRef\"> & {\n /**\n * @generated from field: string id = 1;\n */\n id: string;\n\n /**\n * @generated from field: public_api.v1.PanelCourseItemKind kind = 2;\n */\n kind: PanelCourseItemKind;\n};\n\n/**\n * Describes the message public_api.v1.PanelItemRef.\n * Use `create(PanelItemRefSchema)` to create a new message.\n */\nexport const PanelItemRefSchema: GenMessage<PanelItemRef> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 8);\n\n/**\n * @generated from message public_api.v1.PanelLesson\n */\nexport type PanelLesson = Message<\"public_api.v1.PanelLesson\"> & {\n /**\n * @generated from field: string id = 1;\n */\n id: string;\n\n /**\n * @generated from field: string section_id = 2;\n */\n sectionId: string;\n\n /**\n * @generated from field: string title = 3;\n */\n title: string;\n\n /**\n * Plain text, mainly for video lessons.\n *\n * @generated from field: string description = 4;\n */\n description: string;\n\n /**\n * HTML from the editor of the creator. The client sanitises it before it\n * renders it, the server should too.\n *\n * @generated from field: string content_html = 5;\n */\n contentHtml: string;\n\n /**\n * Unset when the lesson has no video.\n *\n * @generated from field: optional public_api.v1.PanelVideo video = 6;\n */\n video?: PanelVideo;\n\n /**\n * @generated from field: repeated public_api.v1.PanelAttachment attachments = 7;\n */\n attachments: PanelAttachment[];\n\n /**\n * Unset for the first / last item of the course.\n *\n * @generated from field: optional public_api.v1.PanelItemRef prev = 8;\n */\n prev?: PanelItemRef;\n\n /**\n * @generated from field: optional public_api.v1.PanelItemRef next = 9;\n */\n next?: PanelItemRef;\n\n /**\n * @generated from field: bool completed = 10;\n */\n completed: boolean;\n\n /**\n * Where the customer stopped the video, to resume from.\n *\n * @generated from field: int32 position_seconds = 11;\n */\n positionSeconds: number;\n\n /**\n * Rating of the customer, 0 when not rated.\n *\n * @generated from field: int32 my_rating = 12;\n */\n myRating: number;\n\n /**\n * @generated from field: float average_rating = 13;\n */\n averageRating: number;\n\n /**\n * @generated from field: int32 rating_count = 14;\n */\n ratingCount: number;\n\n /**\n * When the signed URLs (video, attachments) stop working; unset when unknown.\n *\n * @generated from field: optional google.protobuf.Timestamp urls_expire_at = 15;\n */\n urlsExpireAt?: Timestamp;\n};\n\n/**\n * Describes the message public_api.v1.PanelLesson.\n * Use `create(PanelLessonSchema)` to create a new message.\n */\nexport const PanelLessonSchema: GenMessage<PanelLesson> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 9);\n\n/**\n * @generated from message public_api.v1.UserPanelGetLessonRequest\n */\nexport type UserPanelGetLessonRequest = Message<\"public_api.v1.UserPanelGetLessonRequest\"> & {\n /**\n * @generated from field: string access_token = 1;\n */\n accessToken: string;\n\n /**\n * @generated from field: string access_id = 2;\n */\n accessId: string;\n\n /**\n * @generated from field: string lesson_id = 3;\n */\n lessonId: string;\n};\n\n/**\n * Describes the message public_api.v1.UserPanelGetLessonRequest.\n * Use `create(UserPanelGetLessonRequestSchema)` to create a new message.\n */\nexport const UserPanelGetLessonRequestSchema: GenMessage<UserPanelGetLessonRequest> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 10);\n\n/**\n * @generated from message public_api.v1.UserPanelGetLessonResponse\n */\nexport type UserPanelGetLessonResponse = Message<\"public_api.v1.UserPanelGetLessonResponse\"> & {\n /**\n * @generated from oneof public_api.v1.UserPanelGetLessonResponse.result\n */\n result: {\n /**\n * @generated from field: public_api.v1.PanelLesson success = 1;\n */\n value: PanelLesson;\n case: \"success\";\n } | {\n /**\n * @generated from field: public_api.v1.PanelCourseFailure failure = 2;\n */\n value: PanelCourseFailure;\n case: \"failure\";\n } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message public_api.v1.UserPanelGetLessonResponse.\n * Use `create(UserPanelGetLessonResponseSchema)` to create a new message.\n */\nexport const UserPanelGetLessonResponseSchema: GenMessage<UserPanelGetLessonResponse> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 11);\n\n/**\n * @generated from message public_api.v1.UserPanelSaveLessonProgressRequest\n */\nexport type UserPanelSaveLessonProgressRequest = Message<\"public_api.v1.UserPanelSaveLessonProgressRequest\"> & {\n /**\n * @generated from field: string access_token = 1;\n */\n accessToken: string;\n\n /**\n * @generated from field: string access_id = 2;\n */\n accessId: string;\n\n /**\n * @generated from field: string lesson_id = 3;\n */\n lessonId: string;\n\n /**\n * Mark the lesson completed. Never un-completes a lesson.\n *\n * @generated from field: bool completed = 4;\n */\n completed: boolean;\n\n /**\n * Playback position in seconds.\n *\n * @generated from field: int32 position_seconds = 5;\n */\n positionSeconds: number;\n\n /**\n * Length of the video in seconds, so the server can compute the percentage.\n *\n * @generated from field: int32 duration_seconds = 6;\n */\n durationSeconds: number;\n};\n\n/**\n * Describes the message public_api.v1.UserPanelSaveLessonProgressRequest.\n * Use `create(UserPanelSaveLessonProgressRequestSchema)` to create a new message.\n */\nexport const UserPanelSaveLessonProgressRequestSchema: GenMessage<UserPanelSaveLessonProgressRequest> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 12);\n\n/**\n * @generated from message public_api.v1.UserPanelSaveLessonProgressResponse\n */\nexport type UserPanelSaveLessonProgressResponse = Message<\"public_api.v1.UserPanelSaveLessonProgressResponse\"> & {\n /**\n * @generated from oneof public_api.v1.UserPanelSaveLessonProgressResponse.result\n */\n result: {\n /**\n * @generated from field: public_api.v1.UserPanelSaveLessonProgressResponse.Result success = 1;\n */\n value: UserPanelSaveLessonProgressResponse_Result;\n case: \"success\";\n } | {\n /**\n * @generated from field: public_api.v1.PanelCourseFailure failure = 2;\n */\n value: PanelCourseFailure;\n case: \"failure\";\n } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message public_api.v1.UserPanelSaveLessonProgressResponse.\n * Use `create(UserPanelSaveLessonProgressResponseSchema)` to create a new message.\n */\nexport const UserPanelSaveLessonProgressResponseSchema: GenMessage<UserPanelSaveLessonProgressResponse> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 13);\n\n/**\n * @generated from message public_api.v1.UserPanelSaveLessonProgressResponse.Result\n */\nexport type UserPanelSaveLessonProgressResponse_Result = Message<\"public_api.v1.UserPanelSaveLessonProgressResponse.Result\"> & {\n /**\n * @generated from field: bool completed = 1;\n */\n completed: boolean;\n\n /**\n * Updated numbers of the whole course.\n *\n * @generated from field: int32 completed_lessons = 2;\n */\n completedLessons: number;\n\n /**\n * @generated from field: int32 progress_percent = 3;\n */\n progressPercent: number;\n};\n\n/**\n * Describes the message public_api.v1.UserPanelSaveLessonProgressResponse.Result.\n * Use `create(UserPanelSaveLessonProgressResponse_ResultSchema)` to create a new message.\n */\nexport const UserPanelSaveLessonProgressResponse_ResultSchema: GenMessage<UserPanelSaveLessonProgressResponse_Result> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 13, 0);\n\n/**\n * @generated from message public_api.v1.PanelChoice\n */\nexport type PanelChoice = Message<\"public_api.v1.PanelChoice\"> & {\n /**\n * @generated from field: string id = 1;\n */\n id: string;\n\n /**\n * @generated from field: string label = 2;\n */\n label: string;\n};\n\n/**\n * Describes the message public_api.v1.PanelChoice.\n * Use `create(PanelChoiceSchema)` to create a new message.\n */\nexport const PanelChoiceSchema: GenMessage<PanelChoice> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 14);\n\n/**\n * Multiple choice question.\n *\n * @generated from message public_api.v1.PanelMcqConfig\n */\nexport type PanelMcqConfig = Message<\"public_api.v1.PanelMcqConfig\"> & {\n /**\n * @generated from field: repeated public_api.v1.PanelChoice choices = 1;\n */\n choices: PanelChoice[];\n\n /**\n * True when more than one choice may be selected (checkboxes), false for\n * radio buttons. Does not tell how many answers are correct.\n *\n * @generated from field: bool multiple = 2;\n */\n multiple: boolean;\n};\n\n/**\n * Describes the message public_api.v1.PanelMcqConfig.\n * Use `create(PanelMcqConfigSchema)` to create a new message.\n */\nexport const PanelMcqConfigSchema: GenMessage<PanelMcqConfig> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 15);\n\n/**\n * A checklist: all required steps have to be checked.\n *\n * @generated from message public_api.v1.PanelTodolistConfig\n */\nexport type PanelTodolistConfig = Message<\"public_api.v1.PanelTodolistConfig\"> & {\n /**\n * @generated from field: repeated public_api.v1.PanelTodolistConfig.Step steps = 1;\n */\n steps: PanelTodolistConfig_Step[];\n};\n\n/**\n * Describes the message public_api.v1.PanelTodolistConfig.\n * Use `create(PanelTodolistConfigSchema)` to create a new message.\n */\nexport const PanelTodolistConfigSchema: GenMessage<PanelTodolistConfig> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 16);\n\n/**\n * @generated from message public_api.v1.PanelTodolistConfig.Step\n */\nexport type PanelTodolistConfig_Step = Message<\"public_api.v1.PanelTodolistConfig.Step\"> & {\n /**\n * @generated from field: string id = 1;\n */\n id: string;\n\n /**\n * @generated from field: string label = 2;\n */\n label: string;\n\n /**\n * @generated from field: bool required = 3;\n */\n required: boolean;\n};\n\n/**\n * Describes the message public_api.v1.PanelTodolistConfig.Step.\n * Use `create(PanelTodolistConfig_StepSchema)` to create a new message.\n */\nexport const PanelTodolistConfig_StepSchema: GenMessage<PanelTodolistConfig_Step> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 16, 0);\n\n/**\n * Text with gaps.\n *\n * @generated from message public_api.v1.PanelFillGapConfig\n */\nexport type PanelFillGapConfig = Message<\"public_api.v1.PanelFillGapConfig\"> & {\n /**\n * Text with placeholders `{{blank_id}}`, one for each id in `blanks`.\n *\n * @generated from field: string template = 1;\n */\n template: string;\n\n /**\n * @generated from field: repeated string blanks = 2;\n */\n blanks: string[];\n};\n\n/**\n * Describes the message public_api.v1.PanelFillGapConfig.\n * Use `create(PanelFillGapConfigSchema)` to create a new message.\n */\nexport const PanelFillGapConfigSchema: GenMessage<PanelFillGapConfig> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 17);\n\n/**\n * Order the items. The server returns them already shuffled.\n *\n * @generated from message public_api.v1.PanelSortOrderConfig\n */\nexport type PanelSortOrderConfig = Message<\"public_api.v1.PanelSortOrderConfig\"> & {\n /**\n * @generated from field: repeated public_api.v1.PanelChoice items = 1;\n */\n items: PanelChoice[];\n};\n\n/**\n * Describes the message public_api.v1.PanelSortOrderConfig.\n * Use `create(PanelSortOrderConfigSchema)` to create a new message.\n */\nexport const PanelSortOrderConfigSchema: GenMessage<PanelSortOrderConfig> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 18);\n\n/**\n * @generated from message public_api.v1.PanelTestQuestion\n */\nexport type PanelTestQuestion = Message<\"public_api.v1.PanelTestQuestion\"> & {\n /**\n * @generated from field: string id = 1;\n */\n id: string;\n\n /**\n * @generated from field: string prompt = 2;\n */\n prompt: string;\n\n /**\n * @generated from field: int32 points = 3;\n */\n points: number;\n\n /**\n * @generated from oneof public_api.v1.PanelTestQuestion.config\n */\n config: {\n /**\n * @generated from field: public_api.v1.PanelMcqConfig mcq = 10;\n */\n value: PanelMcqConfig;\n case: \"mcq\";\n } | {\n /**\n * @generated from field: public_api.v1.PanelTodolistConfig todolist = 11;\n */\n value: PanelTodolistConfig;\n case: \"todolist\";\n } | {\n /**\n * @generated from field: public_api.v1.PanelFillGapConfig fill_gap = 12;\n */\n value: PanelFillGapConfig;\n case: \"fillGap\";\n } | {\n /**\n * @generated from field: public_api.v1.PanelSortOrderConfig sort_order = 13;\n */\n value: PanelSortOrderConfig;\n case: \"sortOrder\";\n } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message public_api.v1.PanelTestQuestion.\n * Use `create(PanelTestQuestionSchema)` to create a new message.\n */\nexport const PanelTestQuestionSchema: GenMessage<PanelTestQuestion> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 19);\n\n/**\n * @generated from message public_api.v1.PanelTest\n */\nexport type PanelTest = Message<\"public_api.v1.PanelTest\"> & {\n /**\n * @generated from field: string id = 1;\n */\n id: string;\n\n /**\n * @generated from field: string title = 2;\n */\n title: string;\n\n /**\n * @generated from field: string description = 3;\n */\n description: string;\n\n /**\n * Passing it unlocks the following sections.\n *\n * @generated from field: bool gating = 4;\n */\n gating: boolean;\n\n /**\n * Minimum score for a pass, 0 to 100.\n *\n * @generated from field: int32 pass_threshold_percent = 5;\n */\n passThresholdPercent: number;\n\n /**\n * 0 means unlimited.\n *\n * @generated from field: int32 attempts_allowed = 6;\n */\n attemptsAllowed: number;\n\n /**\n * Unset when the number of attempts is unlimited.\n *\n * @generated from field: optional int32 attempts_left = 7;\n */\n attemptsLeft?: number;\n\n /**\n * @generated from field: int32 attempt_count = 8;\n */\n attemptCount: number;\n\n /**\n * @generated from field: int32 best_score_percent = 9;\n */\n bestScorePercent: number;\n\n /**\n * @generated from field: bool passed = 10;\n */\n passed: boolean;\n\n /**\n * Questions in order, without the correct answers.\n *\n * @generated from field: repeated public_api.v1.PanelTestQuestion questions = 11;\n */\n questions: PanelTestQuestion[];\n};\n\n/**\n * Describes the message public_api.v1.PanelTest.\n * Use `create(PanelTestSchema)` to create a new message.\n */\nexport const PanelTestSchema: GenMessage<PanelTest> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 20);\n\n/**\n * @generated from message public_api.v1.UserPanelGetTestRequest\n */\nexport type UserPanelGetTestRequest = Message<\"public_api.v1.UserPanelGetTestRequest\"> & {\n /**\n * @generated from field: string access_token = 1;\n */\n accessToken: string;\n\n /**\n * @generated from field: string access_id = 2;\n */\n accessId: string;\n\n /**\n * @generated from field: string test_id = 3;\n */\n testId: string;\n};\n\n/**\n * Describes the message public_api.v1.UserPanelGetTestRequest.\n * Use `create(UserPanelGetTestRequestSchema)` to create a new message.\n */\nexport const UserPanelGetTestRequestSchema: GenMessage<UserPanelGetTestRequest> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 21);\n\n/**\n * @generated from message public_api.v1.UserPanelGetTestResponse\n */\nexport type UserPanelGetTestResponse = Message<\"public_api.v1.UserPanelGetTestResponse\"> & {\n /**\n * @generated from oneof public_api.v1.UserPanelGetTestResponse.result\n */\n result: {\n /**\n * @generated from field: public_api.v1.PanelTest success = 1;\n */\n value: PanelTest;\n case: \"success\";\n } | {\n /**\n * @generated from field: public_api.v1.PanelCourseFailure failure = 2;\n */\n value: PanelCourseFailure;\n case: \"failure\";\n } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message public_api.v1.UserPanelGetTestResponse.\n * Use `create(UserPanelGetTestResponseSchema)` to create a new message.\n */\nexport const UserPanelGetTestResponseSchema: GenMessage<UserPanelGetTestResponse> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 22);\n\n/**\n * @generated from message public_api.v1.PanelMcqAnswer\n */\nexport type PanelMcqAnswer = Message<\"public_api.v1.PanelMcqAnswer\"> & {\n /**\n * @generated from field: repeated string selected_ids = 1;\n */\n selectedIds: string[];\n};\n\n/**\n * Describes the message public_api.v1.PanelMcqAnswer.\n * Use `create(PanelMcqAnswerSchema)` to create a new message.\n */\nexport const PanelMcqAnswerSchema: GenMessage<PanelMcqAnswer> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 23);\n\n/**\n * @generated from message public_api.v1.PanelTodolistAnswer\n */\nexport type PanelTodolistAnswer = Message<\"public_api.v1.PanelTodolistAnswer\"> & {\n /**\n * @generated from field: repeated string checked_ids = 1;\n */\n checkedIds: string[];\n};\n\n/**\n * Describes the message public_api.v1.PanelTodolistAnswer.\n * Use `create(PanelTodolistAnswerSchema)` to create a new message.\n */\nexport const PanelTodolistAnswerSchema: GenMessage<PanelTodolistAnswer> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 24);\n\n/**\n * @generated from message public_api.v1.PanelFillGapAnswer\n */\nexport type PanelFillGapAnswer = Message<\"public_api.v1.PanelFillGapAnswer\"> & {\n /**\n * blank id -> text typed by the customer.\n *\n * @generated from field: map<string, string> values = 1;\n */\n values: { [key: string]: string };\n};\n\n/**\n * Describes the message public_api.v1.PanelFillGapAnswer.\n * Use `create(PanelFillGapAnswerSchema)` to create a new message.\n */\nexport const PanelFillGapAnswerSchema: GenMessage<PanelFillGapAnswer> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 25);\n\n/**\n * @generated from message public_api.v1.PanelSortOrderAnswer\n */\nexport type PanelSortOrderAnswer = Message<\"public_api.v1.PanelSortOrderAnswer\"> & {\n /**\n * @generated from field: repeated string ordered_ids = 1;\n */\n orderedIds: string[];\n};\n\n/**\n * Describes the message public_api.v1.PanelSortOrderAnswer.\n * Use `create(PanelSortOrderAnswerSchema)` to create a new message.\n */\nexport const PanelSortOrderAnswerSchema: GenMessage<PanelSortOrderAnswer> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 26);\n\n/**\n * An answer to one question. The variant has to match the question's config.\n *\n * @generated from message public_api.v1.PanelAnswer\n */\nexport type PanelAnswer = Message<\"public_api.v1.PanelAnswer\"> & {\n /**\n * @generated from field: string question_id = 1;\n */\n questionId: string;\n\n /**\n * @generated from oneof public_api.v1.PanelAnswer.value\n */\n value: {\n /**\n * @generated from field: public_api.v1.PanelMcqAnswer mcq = 10;\n */\n value: PanelMcqAnswer;\n case: \"mcq\";\n } | {\n /**\n * @generated from field: public_api.v1.PanelTodolistAnswer todolist = 11;\n */\n value: PanelTodolistAnswer;\n case: \"todolist\";\n } | {\n /**\n * @generated from field: public_api.v1.PanelFillGapAnswer fill_gap = 12;\n */\n value: PanelFillGapAnswer;\n case: \"fillGap\";\n } | {\n /**\n * @generated from field: public_api.v1.PanelSortOrderAnswer sort_order = 13;\n */\n value: PanelSortOrderAnswer;\n case: \"sortOrder\";\n } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message public_api.v1.PanelAnswer.\n * Use `create(PanelAnswerSchema)` to create a new message.\n */\nexport const PanelAnswerSchema: GenMessage<PanelAnswer> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 27);\n\n/**\n * @generated from message public_api.v1.PanelTestSubmission\n */\nexport type PanelTestSubmission = Message<\"public_api.v1.PanelTestSubmission\"> & {\n /**\n * @generated from field: string id = 1;\n */\n id: string;\n\n /**\n * @generated from field: int32 score_percent = 2;\n */\n scorePercent: number;\n\n /**\n * @generated from field: bool passed = 3;\n */\n passed: boolean;\n\n /**\n * @generated from field: int64 duration_ms = 4;\n */\n durationMs: bigint;\n\n /**\n * @generated from field: google.protobuf.Timestamp created_at = 5;\n */\n createdAt?: Timestamp;\n};\n\n/**\n * Describes the message public_api.v1.PanelTestSubmission.\n * Use `create(PanelTestSubmissionSchema)` to create a new message.\n */\nexport const PanelTestSubmissionSchema: GenMessage<PanelTestSubmission> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 28);\n\n/**\n * @generated from message public_api.v1.PanelQuestionResult\n */\nexport type PanelQuestionResult = Message<\"public_api.v1.PanelQuestionResult\"> & {\n /**\n * @generated from field: string question_id = 1;\n */\n questionId: string;\n\n /**\n * @generated from field: bool correct = 2;\n */\n correct: boolean;\n\n /**\n * @generated from field: int32 points = 3;\n */\n points: number;\n\n /**\n * The correct answer; only set when the \"show answers\" rule of the test\n * allows it (after submit / after pass / never).\n *\n * @generated from field: optional public_api.v1.PanelAnswer correct_answer = 4;\n */\n correctAnswer?: PanelAnswer;\n};\n\n/**\n * Describes the message public_api.v1.PanelQuestionResult.\n * Use `create(PanelQuestionResultSchema)` to create a new message.\n */\nexport const PanelQuestionResultSchema: GenMessage<PanelQuestionResult> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 29);\n\n/**\n * @generated from message public_api.v1.UserPanelSubmitTestRequest\n */\nexport type UserPanelSubmitTestRequest = Message<\"public_api.v1.UserPanelSubmitTestRequest\"> & {\n /**\n * @generated from field: string access_token = 1;\n */\n accessToken: string;\n\n /**\n * @generated from field: string access_id = 2;\n */\n accessId: string;\n\n /**\n * @generated from field: string test_id = 3;\n */\n testId: string;\n\n /**\n * Unanswered questions may be omitted; they score zero.\n *\n * @generated from field: repeated public_api.v1.PanelAnswer answers = 4;\n */\n answers: PanelAnswer[];\n\n /**\n * Time spent on the test.\n *\n * @generated from field: int64 duration_ms = 5;\n */\n durationMs: bigint;\n};\n\n/**\n * Describes the message public_api.v1.UserPanelSubmitTestRequest.\n * Use `create(UserPanelSubmitTestRequestSchema)` to create a new message.\n */\nexport const UserPanelSubmitTestRequestSchema: GenMessage<UserPanelSubmitTestRequest> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 30);\n\n/**\n * @generated from message public_api.v1.UserPanelSubmitTestResponse\n */\nexport type UserPanelSubmitTestResponse = Message<\"public_api.v1.UserPanelSubmitTestResponse\"> & {\n /**\n * @generated from oneof public_api.v1.UserPanelSubmitTestResponse.result\n */\n result: {\n /**\n * @generated from field: public_api.v1.UserPanelSubmitTestResponse.Result success = 1;\n */\n value: UserPanelSubmitTestResponse_Result;\n case: \"success\";\n } | {\n /**\n * @generated from field: public_api.v1.PanelCourseFailure failure = 2;\n */\n value: PanelCourseFailure;\n case: \"failure\";\n } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message public_api.v1.UserPanelSubmitTestResponse.\n * Use `create(UserPanelSubmitTestResponseSchema)` to create a new message.\n */\nexport const UserPanelSubmitTestResponseSchema: GenMessage<UserPanelSubmitTestResponse> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 31);\n\n/**\n * @generated from message public_api.v1.UserPanelSubmitTestResponse.Result\n */\nexport type UserPanelSubmitTestResponse_Result = Message<\"public_api.v1.UserPanelSubmitTestResponse.Result\"> & {\n /**\n * @generated from field: public_api.v1.PanelTestSubmission submission = 1;\n */\n submission?: PanelTestSubmission;\n\n /**\n * @generated from field: repeated public_api.v1.PanelQuestionResult results = 2;\n */\n results: PanelQuestionResult[];\n\n /**\n * Unset when the number of attempts is unlimited.\n *\n * @generated from field: optional int32 attempts_left = 3;\n */\n attemptsLeft?: number;\n};\n\n/**\n * Describes the message public_api.v1.UserPanelSubmitTestResponse.Result.\n * Use `create(UserPanelSubmitTestResponse_ResultSchema)` to create a new message.\n */\nexport const UserPanelSubmitTestResponse_ResultSchema: GenMessage<UserPanelSubmitTestResponse_Result> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 31, 0);\n\n/**\n * @generated from message public_api.v1.UserPanelListTestSubmissionsRequest\n */\nexport type UserPanelListTestSubmissionsRequest = Message<\"public_api.v1.UserPanelListTestSubmissionsRequest\"> & {\n /**\n * @generated from field: string access_token = 1;\n */\n accessToken: string;\n\n /**\n * @generated from field: string access_id = 2;\n */\n accessId: string;\n\n /**\n * @generated from field: string test_id = 3;\n */\n testId: string;\n};\n\n/**\n * Describes the message public_api.v1.UserPanelListTestSubmissionsRequest.\n * Use `create(UserPanelListTestSubmissionsRequestSchema)` to create a new message.\n */\nexport const UserPanelListTestSubmissionsRequestSchema: GenMessage<UserPanelListTestSubmissionsRequest> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 32);\n\n/**\n * @generated from message public_api.v1.UserPanelListTestSubmissionsResponse\n */\nexport type UserPanelListTestSubmissionsResponse = Message<\"public_api.v1.UserPanelListTestSubmissionsResponse\"> & {\n /**\n * @generated from oneof public_api.v1.UserPanelListTestSubmissionsResponse.result\n */\n result: {\n /**\n * @generated from field: public_api.v1.UserPanelListTestSubmissionsResponse.Result success = 1;\n */\n value: UserPanelListTestSubmissionsResponse_Result;\n case: \"success\";\n } | {\n /**\n * @generated from field: public_api.v1.PanelCourseFailure failure = 2;\n */\n value: PanelCourseFailure;\n case: \"failure\";\n } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message public_api.v1.UserPanelListTestSubmissionsResponse.\n * Use `create(UserPanelListTestSubmissionsResponseSchema)` to create a new message.\n */\nexport const UserPanelListTestSubmissionsResponseSchema: GenMessage<UserPanelListTestSubmissionsResponse> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 33);\n\n/**\n * @generated from message public_api.v1.UserPanelListTestSubmissionsResponse.Result\n */\nexport type UserPanelListTestSubmissionsResponse_Result = Message<\"public_api.v1.UserPanelListTestSubmissionsResponse.Result\"> & {\n /**\n * Newest first.\n *\n * @generated from field: repeated public_api.v1.PanelTestSubmission submissions = 1;\n */\n submissions: PanelTestSubmission[];\n\n /**\n * @generated from field: int32 best_score_percent = 2;\n */\n bestScorePercent: number;\n\n /**\n * @generated from field: int32 attempt_count = 3;\n */\n attemptCount: number;\n};\n\n/**\n * Describes the message public_api.v1.UserPanelListTestSubmissionsResponse.Result.\n * Use `create(UserPanelListTestSubmissionsResponse_ResultSchema)` to create a new message.\n */\nexport const UserPanelListTestSubmissionsResponse_ResultSchema: GenMessage<UserPanelListTestSubmissionsResponse_Result> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 33, 0);\n\n/**\n * @generated from message public_api.v1.PanelQuestionAnswer\n */\nexport type PanelQuestionAnswer = Message<\"public_api.v1.PanelQuestionAnswer\"> & {\n /**\n * @generated from field: string content = 1;\n */\n content: string;\n\n /**\n * @generated from field: google.protobuf.Timestamp created_at = 2;\n */\n createdAt?: Timestamp;\n};\n\n/**\n * Describes the message public_api.v1.PanelQuestionAnswer.\n * Use `create(PanelQuestionAnswerSchema)` to create a new message.\n */\nexport const PanelQuestionAnswerSchema: GenMessage<PanelQuestionAnswer> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 34);\n\n/**\n * @generated from message public_api.v1.PanelLessonQuestion\n */\nexport type PanelLessonQuestion = Message<\"public_api.v1.PanelLessonQuestion\"> & {\n /**\n * @generated from field: string id = 1;\n */\n id: string;\n\n /**\n * @generated from field: string content = 2;\n */\n content: string;\n\n /**\n * Display name resolved by Bitsnap; empty for an anonymous customer.\n *\n * @generated from field: string author_name = 3;\n */\n authorName: string;\n\n /**\n * @generated from field: bool is_mine = 4;\n */\n isMine: boolean;\n\n /**\n * @generated from field: google.protobuf.Timestamp created_at = 5;\n */\n createdAt?: Timestamp;\n\n /**\n * Unset until the course team answers.\n *\n * @generated from field: optional public_api.v1.PanelQuestionAnswer answer = 6;\n */\n answer?: PanelQuestionAnswer;\n};\n\n/**\n * Describes the message public_api.v1.PanelLessonQuestion.\n * Use `create(PanelLessonQuestionSchema)` to create a new message.\n */\nexport const PanelLessonQuestionSchema: GenMessage<PanelLessonQuestion> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 35);\n\n/**\n * @generated from message public_api.v1.UserPanelListLessonQuestionsRequest\n */\nexport type UserPanelListLessonQuestionsRequest = Message<\"public_api.v1.UserPanelListLessonQuestionsRequest\"> & {\n /**\n * @generated from field: string access_token = 1;\n */\n accessToken: string;\n\n /**\n * @generated from field: string access_id = 2;\n */\n accessId: string;\n\n /**\n * @generated from field: string lesson_id = 3;\n */\n lessonId: string;\n};\n\n/**\n * Describes the message public_api.v1.UserPanelListLessonQuestionsRequest.\n * Use `create(UserPanelListLessonQuestionsRequestSchema)` to create a new message.\n */\nexport const UserPanelListLessonQuestionsRequestSchema: GenMessage<UserPanelListLessonQuestionsRequest> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 36);\n\n/**\n * @generated from message public_api.v1.UserPanelListLessonQuestionsResponse\n */\nexport type UserPanelListLessonQuestionsResponse = Message<\"public_api.v1.UserPanelListLessonQuestionsResponse\"> & {\n /**\n * @generated from oneof public_api.v1.UserPanelListLessonQuestionsResponse.result\n */\n result: {\n /**\n * @generated from field: public_api.v1.UserPanelListLessonQuestionsResponse.Result success = 1;\n */\n value: UserPanelListLessonQuestionsResponse_Result;\n case: \"success\";\n } | {\n /**\n * @generated from field: public_api.v1.PanelCourseFailure failure = 2;\n */\n value: PanelCourseFailure;\n case: \"failure\";\n } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message public_api.v1.UserPanelListLessonQuestionsResponse.\n * Use `create(UserPanelListLessonQuestionsResponseSchema)` to create a new message.\n */\nexport const UserPanelListLessonQuestionsResponseSchema: GenMessage<UserPanelListLessonQuestionsResponse> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 37);\n\n/**\n * @generated from message public_api.v1.UserPanelListLessonQuestionsResponse.Result\n */\nexport type UserPanelListLessonQuestionsResponse_Result = Message<\"public_api.v1.UserPanelListLessonQuestionsResponse.Result\"> & {\n /**\n * Newest first.\n *\n * @generated from field: repeated public_api.v1.PanelLessonQuestion questions = 1;\n */\n questions: PanelLessonQuestion[];\n};\n\n/**\n * Describes the message public_api.v1.UserPanelListLessonQuestionsResponse.Result.\n * Use `create(UserPanelListLessonQuestionsResponse_ResultSchema)` to create a new message.\n */\nexport const UserPanelListLessonQuestionsResponse_ResultSchema: GenMessage<UserPanelListLessonQuestionsResponse_Result> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 37, 0);\n\n/**\n * @generated from message public_api.v1.UserPanelAskLessonQuestionRequest\n */\nexport type UserPanelAskLessonQuestionRequest = Message<\"public_api.v1.UserPanelAskLessonQuestionRequest\"> & {\n /**\n * @generated from field: string access_token = 1;\n */\n accessToken: string;\n\n /**\n * @generated from field: string access_id = 2;\n */\n accessId: string;\n\n /**\n * @generated from field: string lesson_id = 3;\n */\n lessonId: string;\n\n /**\n * @generated from field: string content = 4;\n */\n content: string;\n};\n\n/**\n * Describes the message public_api.v1.UserPanelAskLessonQuestionRequest.\n * Use `create(UserPanelAskLessonQuestionRequestSchema)` to create a new message.\n */\nexport const UserPanelAskLessonQuestionRequestSchema: GenMessage<UserPanelAskLessonQuestionRequest> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 38);\n\n/**\n * @generated from message public_api.v1.UserPanelAskLessonQuestionResponse\n */\nexport type UserPanelAskLessonQuestionResponse = Message<\"public_api.v1.UserPanelAskLessonQuestionResponse\"> & {\n /**\n * @generated from oneof public_api.v1.UserPanelAskLessonQuestionResponse.result\n */\n result: {\n /**\n * @generated from field: public_api.v1.PanelLessonQuestion success = 1;\n */\n value: PanelLessonQuestion;\n case: \"success\";\n } | {\n /**\n * @generated from field: public_api.v1.PanelCourseFailure failure = 2;\n */\n value: PanelCourseFailure;\n case: \"failure\";\n } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message public_api.v1.UserPanelAskLessonQuestionResponse.\n * Use `create(UserPanelAskLessonQuestionResponseSchema)` to create a new message.\n */\nexport const UserPanelAskLessonQuestionResponseSchema: GenMessage<UserPanelAskLessonQuestionResponse> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 39);\n\n/**\n * @generated from message public_api.v1.UserPanelRateLessonRequest\n */\nexport type UserPanelRateLessonRequest = Message<\"public_api.v1.UserPanelRateLessonRequest\"> & {\n /**\n * @generated from field: string access_token = 1;\n */\n accessToken: string;\n\n /**\n * @generated from field: string access_id = 2;\n */\n accessId: string;\n\n /**\n * @generated from field: string lesson_id = 3;\n */\n lessonId: string;\n\n /**\n * 1 to 5. Rating again replaces the previous rating.\n *\n * @generated from field: int32 score = 4;\n */\n score: number;\n\n /**\n * @generated from field: string comment = 5;\n */\n comment: string;\n};\n\n/**\n * Describes the message public_api.v1.UserPanelRateLessonRequest.\n * Use `create(UserPanelRateLessonRequestSchema)` to create a new message.\n */\nexport const UserPanelRateLessonRequestSchema: GenMessage<UserPanelRateLessonRequest> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 40);\n\n/**\n * @generated from message public_api.v1.UserPanelRateLessonResponse\n */\nexport type UserPanelRateLessonResponse = Message<\"public_api.v1.UserPanelRateLessonResponse\"> & {\n /**\n * @generated from oneof public_api.v1.UserPanelRateLessonResponse.result\n */\n result: {\n /**\n * @generated from field: public_api.v1.UserPanelRateLessonResponse.Result success = 1;\n */\n value: UserPanelRateLessonResponse_Result;\n case: \"success\";\n } | {\n /**\n * @generated from field: public_api.v1.PanelCourseFailure failure = 2;\n */\n value: PanelCourseFailure;\n case: \"failure\";\n } | { case: undefined; value?: undefined };\n};\n\n/**\n * Describes the message public_api.v1.UserPanelRateLessonResponse.\n * Use `create(UserPanelRateLessonResponseSchema)` to create a new message.\n */\nexport const UserPanelRateLessonResponseSchema: GenMessage<UserPanelRateLessonResponse> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 41);\n\n/**\n * @generated from message public_api.v1.UserPanelRateLessonResponse.Result\n */\nexport type UserPanelRateLessonResponse_Result = Message<\"public_api.v1.UserPanelRateLessonResponse.Result\"> & {\n /**\n * @generated from field: int32 my_rating = 1;\n */\n myRating: number;\n\n /**\n * @generated from field: float average_rating = 2;\n */\n averageRating: number;\n\n /**\n * @generated from field: int32 rating_count = 3;\n */\n ratingCount: number;\n};\n\n/**\n * Describes the message public_api.v1.UserPanelRateLessonResponse.Result.\n * Use `create(UserPanelRateLessonResponse_ResultSchema)` to create a new message.\n */\nexport const UserPanelRateLessonResponse_ResultSchema: GenMessage<UserPanelRateLessonResponse_Result> = /*@__PURE__*/\n messageDesc(file_public_v1_user_panel_course, 41, 0);\n\n/**\n * @generated from enum public_api.v1.PanelCourseFailure\n */\nexport enum PanelCourseFailure {\n /**\n * @generated from enum value: PANEL_COURSE_FAILURE_UNKNOWN = 0;\n */\n UNKNOWN = 0,\n\n /**\n * Missing, expired or foreign access token, or no paid access to access_id.\n *\n * @generated from enum value: PANEL_COURSE_FAILURE_UNAUTHORIZED = 1;\n */\n UNAUTHORIZED = 1,\n\n /**\n * @generated from enum value: PANEL_COURSE_FAILURE_COURSE_NOT_FOUND = 2;\n */\n COURSE_NOT_FOUND = 2,\n\n /**\n * Unknown lesson or test (or one of another course).\n *\n * @generated from enum value: PANEL_COURSE_FAILURE_ITEM_NOT_FOUND = 3;\n */\n ITEM_NOT_FOUND = 3,\n\n /**\n * The item is locked by a gating test (see PanelCourseItem.lock_reason).\n *\n * @generated from enum value: PANEL_COURSE_FAILURE_ITEM_LOCKED = 4;\n */\n ITEM_LOCKED = 4,\n\n /**\n * SubmitTest after the last allowed attempt.\n *\n * @generated from enum value: PANEL_COURSE_FAILURE_NO_ATTEMPTS_LEFT = 5;\n */\n NO_ATTEMPTS_LEFT = 5,\n\n /**\n * Malformed request, e.g. an answer to an unknown question, a rating out of\n * 1..5 or an empty question.\n *\n * @generated from enum value: PANEL_COURSE_FAILURE_INVALID_ARGUMENT = 6;\n */\n INVALID_ARGUMENT = 6,\n\n /**\n * @generated from enum value: PANEL_COURSE_FAILURE_RATE_LIMITED = 7;\n */\n RATE_LIMITED = 7,\n\n /**\n * LMS is not reachable. The client may retry.\n *\n * @generated from enum value: PANEL_COURSE_FAILURE_UNAVAILABLE = 8;\n */\n UNAVAILABLE = 8,\n}\n\n/**\n * Describes the enum public_api.v1.PanelCourseFailure.\n */\nexport const PanelCourseFailureSchema: GenEnum<PanelCourseFailure> = /*@__PURE__*/\n enumDesc(file_public_v1_user_panel_course, 0);\n\n/**\n * @generated from enum public_api.v1.PanelCourseType\n */\nexport enum PanelCourseType {\n /**\n * @generated from enum value: PANEL_COURSE_TYPE_UNKNOWN = 0;\n */\n UNKNOWN = 0,\n\n /**\n * Modules of lessons with HTML content, an optional video, attachments and\n * tests.\n *\n * @generated from enum value: PANEL_COURSE_TYPE_STANDARD = 1;\n */\n STANDARD = 1,\n\n /**\n * Sections of videos: a lesson is a video with a description, a thumbnail and\n * attachments.\n *\n * @generated from enum value: PANEL_COURSE_TYPE_VIDEO = 2;\n */\n VIDEO = 2,\n}\n\n/**\n * Describes the enum public_api.v1.PanelCourseType.\n */\nexport const PanelCourseTypeSchema: GenEnum<PanelCourseType> = /*@__PURE__*/\n enumDesc(file_public_v1_user_panel_course, 1);\n\n/**\n * @generated from enum public_api.v1.PanelCourseItemKind\n */\nexport enum PanelCourseItemKind {\n /**\n * @generated from enum value: PANEL_COURSE_ITEM_KIND_UNKNOWN = 0;\n */\n UNKNOWN = 0,\n\n /**\n * @generated from enum value: PANEL_COURSE_ITEM_KIND_LESSON = 1;\n */\n LESSON = 1,\n\n /**\n * @generated from enum value: PANEL_COURSE_ITEM_KIND_TEST = 2;\n */\n TEST = 2,\n}\n\n/**\n * Describes the enum public_api.v1.PanelCourseItemKind.\n */\nexport const PanelCourseItemKindSchema: GenEnum<PanelCourseItemKind> = /*@__PURE__*/\n enumDesc(file_public_v1_user_panel_course, 2);\n\n/**\n * Course view of the customer panel (`bitsnap-react/course`).\n *\n * DRAFT. The panel talks only to Bitsnap's /api/rpc. Bitsnap checks the paid\n * access of the customer on every call, calls the internal LMS server to\n * server and returns content with URLs that are already signed:\n * - attachments: presigned S3 URLs,\n * - video: a Cloudflare Stream HLS manifest URL with a signed token.\n * The client never sees LMS ids or LMS credentials.\n *\n * Authentication follows the other UserPanel* calls (UserPanelGetCourseDetails,\n * UserPanelGetFileDetails): `access_token` is the panel session token and\n * `access_id` is UserProduct.access_id of the purchased course. The course is\n * resolved on the server from `access_id`, so there is no project_id and no\n * course_id in the requests. All other ids are opaque strings returned by\n * GetCourse.\n *\n * Every response is `oneof result { success, failure }` like the other panel\n * calls. Gateway level errors (network, 5xx) surface as Connect errors.\n *\n * @generated from service public_api.v1.UserPanelCourseService\n */\nexport const UserPanelCourseService: GenService<{\n /**\n * Course structure with the progress of the customer.\n *\n * @generated from rpc public_api.v1.UserPanelCourseService.GetCourse\n */\n getCourse: {\n methodKind: \"unary\";\n input: typeof UserPanelGetCourseRequestSchema;\n output: typeof UserPanelGetCourseResponseSchema;\n },\n /**\n * One lesson: content, signed video and attachment URLs.\n *\n * @generated from rpc public_api.v1.UserPanelCourseService.GetLesson\n */\n getLesson: {\n methodKind: \"unary\";\n input: typeof UserPanelGetLessonRequestSchema;\n output: typeof UserPanelGetLessonResponseSchema;\n },\n /**\n * Reports the playback position and/or completion of a lesson.\n *\n * @generated from rpc public_api.v1.UserPanelCourseService.SaveLessonProgress\n */\n saveLessonProgress: {\n methodKind: \"unary\";\n input: typeof UserPanelSaveLessonProgressRequestSchema;\n output: typeof UserPanelSaveLessonProgressResponseSchema;\n },\n /**\n * A test without the correct answers.\n *\n * @generated from rpc public_api.v1.UserPanelCourseService.GetTest\n */\n getTest: {\n methodKind: \"unary\";\n input: typeof UserPanelGetTestRequestSchema;\n output: typeof UserPanelGetTestResponseSchema;\n },\n /**\n * @generated from rpc public_api.v1.UserPanelCourseService.SubmitTest\n */\n submitTest: {\n methodKind: \"unary\";\n input: typeof UserPanelSubmitTestRequestSchema;\n output: typeof UserPanelSubmitTestResponseSchema;\n },\n /**\n * @generated from rpc public_api.v1.UserPanelCourseService.ListTestSubmissions\n */\n listTestSubmissions: {\n methodKind: \"unary\";\n input: typeof UserPanelListTestSubmissionsRequestSchema;\n output: typeof UserPanelListTestSubmissionsResponseSchema;\n },\n /**\n * @generated from rpc public_api.v1.UserPanelCourseService.ListLessonQuestions\n */\n listLessonQuestions: {\n methodKind: \"unary\";\n input: typeof UserPanelListLessonQuestionsRequestSchema;\n output: typeof UserPanelListLessonQuestionsResponseSchema;\n },\n /**\n * @generated from rpc public_api.v1.UserPanelCourseService.AskLessonQuestion\n */\n askLessonQuestion: {\n methodKind: \"unary\";\n input: typeof UserPanelAskLessonQuestionRequestSchema;\n output: typeof UserPanelAskLessonQuestionResponseSchema;\n },\n /**\n * @generated from rpc public_api.v1.UserPanelCourseService.RateLesson\n */\n rateLesson: {\n methodKind: \"unary\";\n input: typeof UserPanelRateLessonRequestSchema;\n output: typeof UserPanelRateLessonResponseSchema;\n },\n}> = /*@__PURE__*/\n serviceDesc(file_public_v1_user_panel_course, 0);\n\n","import { Code, ConnectError, type Client } from \"@connectrpc/connect\";\nimport { timestampDate } from \"@bufbuild/protobuf/wkt\";\nimport {\n PanelCourseFailure,\n PanelCourseItemKind,\n PanelCourseType,\n type PanelAnswer,\n type PanelCourse,\n type PanelCourseItem,\n type PanelItemRef,\n type PanelLesson,\n type PanelLessonQuestion,\n type PanelTest,\n type PanelTestQuestion,\n type PanelTestSubmission,\n type UserPanelCourseService,\n} from \"../gen/proto/public/v1/user_panel_course_pb\";\nimport {\n CourseError,\n type Course,\n type CourseDataSource,\n type CourseErrorCode,\n type CourseItem,\n type ItemRef,\n type Lesson,\n type LessonQuestion,\n type QuestionAnswer,\n type Test,\n type TestAnswers,\n type TestQuestion,\n type TestSubmission,\n} from \"./types\";\n\n/** The part of the generated Connect client the data source calls. */\nexport type CourseRpcClient = Client<typeof UserPanelCourseService>;\n\nexport type CourseAuth = {\n /** `UserProduct.access_id` of the purchased course. */\n accessId: string;\n /** Panel session token; a function is read on every call. */\n accessToken: string | (() => string | undefined);\n};\n\nconst FAILURE_CODES: Record<PanelCourseFailure, CourseErrorCode> = {\n [PanelCourseFailure.UNKNOWN]: \"unknown\",\n [PanelCourseFailure.UNAUTHORIZED]: \"unauthorized\",\n [PanelCourseFailure.COURSE_NOT_FOUND]: \"course_not_found\",\n [PanelCourseFailure.ITEM_NOT_FOUND]: \"item_not_found\",\n [PanelCourseFailure.ITEM_LOCKED]: \"item_locked\",\n [PanelCourseFailure.NO_ATTEMPTS_LEFT]: \"no_attempts_left\",\n [PanelCourseFailure.INVALID_ARGUMENT]: \"invalid_argument\",\n [PanelCourseFailure.RATE_LIMITED]: \"rate_limited\",\n [PanelCourseFailure.UNAVAILABLE]: \"unavailable\",\n};\n\nexport function failureToCode(failure: PanelCourseFailure): CourseErrorCode {\n return FAILURE_CODES[failure] ?? \"unknown\";\n}\n\n/** Maps a transport error to a `CourseError`; other errors pass through. */\nexport function toCourseError(error: unknown): unknown {\n if (error instanceof CourseError) {\n return error;\n }\n const connectError = ConnectError.from(error);\n switch (connectError.code) {\n case Code.Unauthenticated:\n case Code.PermissionDenied:\n return new CourseError(\"unauthorized\", connectError.rawMessage);\n case Code.NotFound:\n return new CourseError(\"course_not_found\", connectError.rawMessage);\n case Code.ResourceExhausted:\n return new CourseError(\"rate_limited\", connectError.rawMessage);\n case Code.Unavailable:\n case Code.DeadlineExceeded:\n return new CourseError(\"unavailable\", connectError.rawMessage);\n default:\n return error;\n }\n}\n\ntype Result<T> = {\n result:\n | { case: \"success\"; value: T }\n | { case: \"failure\"; value: PanelCourseFailure }\n | { case: undefined; value?: undefined };\n};\n\n/** The success value of a response, or a `CourseError` for a failure. */\nexport function unwrap<T>(response: Result<T>): T {\n const { result } = response;\n if (result.case === \"success\") {\n return result.value;\n }\n if (result.case === \"failure\") {\n throw new CourseError(failureToCode(result.value));\n }\n throw new CourseError(\"unknown\", \"empty response\");\n}\n\n// Mapping from the protobuf messages\n\nfunction mapItem(item: PanelCourseItem): CourseItem {\n return {\n id: item.id,\n kind:\n item.kind === PanelCourseItemKind.TEST ? \"test\" : \"lesson\",\n title: item.title,\n isFree: item.isFree,\n locked: item.locked,\n lockReason:\n item.locked && item.lockReason != null\n ? {\n gatingTestId: item.lockReason.gatingTestId,\n gatingTestTitle: item.lockReason.gatingTestTitle,\n }\n : undefined,\n completed: item.completed,\n durationSeconds: item.durationSeconds,\n thumbnailUrl: item.thumbnailUrl || undefined,\n gating: item.gating,\n };\n}\n\nexport function mapCourse(course: PanelCourse): Course {\n return {\n id: course.id,\n type: course.type === PanelCourseType.VIDEO ? \"video\" : \"standard\",\n title: course.title,\n description: course.description,\n coverUrl: course.coverUrl || undefined,\n totalLessons: course.totalLessons,\n completedLessons: course.completedLessons,\n progressPercent: course.progressPercent,\n sections: course.sections.map((section) => ({\n id: section.id,\n title: section.title,\n description: section.description,\n items: section.items.map(mapItem),\n })),\n finalItems: course.finalItems.map(mapItem),\n resumeItemId: course.resumeItemId,\n };\n}\n\nfunction mapRef(ref: PanelItemRef | undefined): ItemRef | undefined {\n return ref == null\n ? undefined\n : {\n id: ref.id,\n kind: ref.kind === PanelCourseItemKind.TEST ? \"test\" : \"lesson\",\n };\n}\n\nexport function mapLesson(lesson: PanelLesson): Lesson {\n return {\n id: lesson.id,\n sectionId: lesson.sectionId,\n title: lesson.title,\n description: lesson.description,\n contentHtml: lesson.contentHtml,\n video:\n lesson.video != null && lesson.video.hlsUrl !== \"\"\n ? {\n hlsUrl: lesson.video.hlsUrl,\n posterUrl: lesson.video.posterUrl || undefined,\n durationSeconds: lesson.video.durationSeconds,\n }\n : undefined,\n attachments: lesson.attachments.map((a) => ({\n id: a.id,\n name: a.name,\n size: Number(a.size),\n url: a.url,\n })),\n prev: mapRef(lesson.prev),\n next: mapRef(lesson.next),\n completed: lesson.completed,\n positionSeconds: lesson.positionSeconds,\n myRating: lesson.myRating,\n averageRating: lesson.averageRating,\n ratingCount: lesson.ratingCount,\n urlsExpireAt:\n lesson.urlsExpireAt != null ? timestampDate(lesson.urlsExpireAt) : undefined,\n };\n}\n\nfunction mapQuestion(question: PanelTestQuestion): TestQuestion | undefined {\n const base = { id: question.id, prompt: question.prompt, points: question.points };\n const { config } = question;\n switch (config.case) {\n case \"mcq\":\n return {\n ...base,\n config: {\n type: \"mcq\",\n choices: config.value.choices.map((c) => ({ id: c.id, label: c.label })),\n multiple: config.value.multiple,\n },\n };\n case \"todolist\":\n return {\n ...base,\n config: {\n type: \"todolist\",\n steps: config.value.steps.map((s) => ({\n id: s.id,\n label: s.label,\n required: s.required,\n })),\n },\n };\n case \"fillGap\":\n return {\n ...base,\n config: {\n type: \"fill_gap\",\n template: config.value.template,\n blanks: [...config.value.blanks],\n },\n };\n case \"sortOrder\":\n return {\n ...base,\n config: {\n type: \"sort_order\",\n items: config.value.items.map((c) => ({ id: c.id, label: c.label })),\n },\n };\n default:\n // A kind this version of the panel does not know.\n return undefined;\n }\n}\n\nexport function mapTest(test: PanelTest): Test {\n return {\n id: test.id,\n title: test.title,\n description: test.description,\n gating: test.gating,\n passThresholdPercent: test.passThresholdPercent,\n attemptsAllowed: test.attemptsAllowed,\n attemptsLeft: test.attemptsLeft,\n attemptCount: test.attemptCount,\n bestScorePercent: test.bestScorePercent,\n passed: test.passed,\n questions: test.questions\n .map(mapQuestion)\n .filter((q): q is TestQuestion => q != null),\n };\n}\n\nexport function mapSubmission(submission: PanelTestSubmission): TestSubmission {\n return {\n id: submission.id,\n scorePercent: submission.scorePercent,\n passed: submission.passed,\n durationMs: Number(submission.durationMs),\n createdAt:\n submission.createdAt != null ? timestampDate(submission.createdAt) : undefined,\n };\n}\n\nexport function mapAnswerFromProto(answer: PanelAnswer): QuestionAnswer | undefined {\n const { value } = answer;\n switch (value.case) {\n case \"mcq\":\n return { type: \"mcq\", selectedIds: [...value.value.selectedIds] };\n case \"todolist\":\n return { type: \"todolist\", checkedIds: [...value.value.checkedIds] };\n case \"fillGap\":\n return { type: \"fill_gap\", values: { ...value.value.values } };\n case \"sortOrder\":\n return { type: \"sort_order\", orderedIds: [...value.value.orderedIds] };\n default:\n return undefined;\n }\n}\n\n/** Request shape of an answer (plain init object of `PanelAnswer`). */\nexport function mapAnswerToProto(questionId: string, answer: QuestionAnswer) {\n switch (answer.type) {\n case \"mcq\":\n return {\n questionId,\n value: { case: \"mcq\" as const, value: { selectedIds: answer.selectedIds } },\n };\n case \"todolist\":\n return {\n questionId,\n value: { case: \"todolist\" as const, value: { checkedIds: answer.checkedIds } },\n };\n case \"fill_gap\":\n return {\n questionId,\n value: { case: \"fillGap\" as const, value: { values: answer.values } },\n };\n case \"sort_order\":\n return {\n questionId,\n value: { case: \"sortOrder\" as const, value: { orderedIds: answer.orderedIds } },\n };\n }\n}\n\nfunction mapLessonQuestion(question: PanelLessonQuestion): LessonQuestion {\n return {\n id: question.id,\n content: question.content,\n authorName: question.authorName,\n isMine: question.isMine,\n createdAt:\n question.createdAt != null ? timestampDate(question.createdAt) : undefined,\n answer:\n question.answer != null\n ? {\n content: question.answer.content,\n createdAt:\n question.answer.createdAt != null\n ? timestampDate(question.answer.createdAt)\n : undefined,\n }\n : undefined,\n };\n}\n\n/**\n * Data source over `UserPanelCourseService`. Every call carries the access id\n * and the panel session token; failures become `CourseError`.\n */\nexport function createCourseDataSource(\n client: CourseRpcClient,\n auth: CourseAuth,\n): CourseDataSource {\n const credentials = () => ({\n accessId: auth.accessId,\n accessToken:\n typeof auth.accessToken === \"function\"\n ? (auth.accessToken() ?? \"\")\n : auth.accessToken,\n });\n\n async function call<T>(run: () => Promise<T>): Promise<T> {\n try {\n return await run();\n } catch (error) {\n throw toCourseError(error);\n }\n }\n\n return {\n getCourse: () =>\n call(async () => mapCourse(unwrap(await client.getCourse(credentials())))),\n\n getLesson: (lessonId) =>\n call(async () =>\n mapLesson(unwrap(await client.getLesson({ ...credentials(), lessonId }))),\n ),\n\n saveProgress: (input) =>\n call(async () => {\n const saved = unwrap(\n await client.saveLessonProgress({ ...credentials(), ...input }),\n );\n return {\n completed: saved.completed,\n completedLessons: saved.completedLessons,\n progressPercent: saved.progressPercent,\n };\n }),\n\n getTest: (testId) =>\n call(async () =>\n mapTest(unwrap(await client.getTest({ ...credentials(), testId }))),\n ),\n\n submitTest: (testId, answers: TestAnswers, durationMs) =>\n call(async () => {\n const result = unwrap(\n await client.submitTest({\n ...credentials(),\n testId,\n durationMs: BigInt(Math.max(0, Math.round(durationMs))),\n answers: Object.entries(answers).map(([questionId, answer]) =>\n mapAnswerToProto(questionId, answer),\n ),\n }),\n );\n if (result.submission == null) {\n throw new CourseError(\"unknown\", \"missing submission\");\n }\n return {\n submission: mapSubmission(result.submission),\n results: result.results.map((r) => ({\n questionId: r.questionId,\n correct: r.correct,\n points: r.points,\n correctAnswer:\n r.correctAnswer != null\n ? mapAnswerFromProto(r.correctAnswer)\n : undefined,\n })),\n attemptsLeft: result.attemptsLeft,\n };\n }),\n\n listSubmissions: (testId) =>\n call(async () => {\n const list = unwrap(\n await client.listTestSubmissions({ ...credentials(), testId }),\n );\n return {\n submissions: list.submissions.map(mapSubmission),\n bestScorePercent: list.bestScorePercent,\n attemptCount: list.attemptCount,\n };\n }),\n\n listQuestions: (lessonId) =>\n call(async () =>\n unwrap(\n await client.listLessonQuestions({ ...credentials(), lessonId }),\n ).questions.map(mapLessonQuestion),\n ),\n\n askQuestion: (lessonId, content) =>\n call(async () =>\n mapLessonQuestion(\n unwrap(\n await client.askLessonQuestion({\n ...credentials(),\n lessonId,\n content,\n }),\n ),\n ),\n ),\n\n rate: (lessonId, score, comment = \"\") =>\n call(async () => {\n const rating = unwrap(\n await client.rateLesson({ ...credentials(), lessonId, score, comment }),\n );\n return {\n myRating: rating.myRating,\n averageRating: rating.averageRating,\n ratingCount: rating.ratingCount,\n };\n }),\n };\n}\n","/**\n * Domain model of the course view. It is independent of the generated\n * protobuf messages, so a creator preview can feed `PanelCourse` through its\n * own `CourseDataSource` without the RPC layer.\n */\n\nexport type CourseType = \"standard\" | \"video\";\n\nexport type CourseItemKind = \"lesson\" | \"test\";\n\n/** The gating test that has to be passed before the item opens. */\nexport type LockReason = {\n gatingTestId: string;\n gatingTestTitle: string;\n};\n\n/** A lesson or a test in the list of a course. */\nexport type CourseItem = {\n /** Unique among all items of the course. */\n id: string;\n kind: CourseItemKind;\n title: string;\n isFree: boolean;\n locked: boolean;\n lockReason?: LockReason;\n /** A lesson: completed. A test: passed. */\n completed: boolean;\n /** Video length, 0 when unknown. */\n durationSeconds: number;\n thumbnailUrl?: string;\n /** Tests only: passing it unlocks the following sections. */\n gating: boolean;\n};\n\n/** A module (standard course) or a section (video course). */\nexport type CourseSection = {\n id: string;\n title: string;\n description: string;\n items: CourseItem[];\n};\n\nexport type Course = {\n id: string;\n type: CourseType;\n title: string;\n description: string;\n coverUrl?: string;\n totalLessons: number;\n completedLessons: number;\n /** 0 to 100. */\n progressPercent: number;\n sections: CourseSection[];\n /** Tests of the whole course, after the last section. */\n finalItems: CourseItem[];\n /** Item to continue with; empty when everything is done. */\n resumeItemId: string;\n};\n\nexport type ItemRef = {\n id: string;\n kind: CourseItemKind;\n};\n\nexport type LessonVideo = {\n hlsUrl: string;\n posterUrl?: string;\n durationSeconds: number;\n};\n\nexport type LessonAttachment = {\n id: string;\n name: string;\n /** Bytes. */\n size: number;\n url: string;\n};\n\nexport type Lesson = {\n id: string;\n sectionId: string;\n title: string;\n description: string;\n /** Raw HTML of the editor. Always pass it through `sanitizeLessonHtml`. */\n contentHtml: string;\n video?: LessonVideo;\n attachments: LessonAttachment[];\n prev?: ItemRef;\n next?: ItemRef;\n completed: boolean;\n /** Where the customer stopped the video. */\n positionSeconds: number;\n /** 0 when the customer has not rated the lesson. */\n myRating: number;\n averageRating: number;\n ratingCount: number;\n /** When the signed URLs stop working. */\n urlsExpireAt?: Date;\n};\n\nexport type SaveProgressInput = {\n lessonId: string;\n completed: boolean;\n positionSeconds: number;\n durationSeconds: number;\n};\n\nexport type ProgressSaved = {\n completed: boolean;\n completedLessons: number;\n progressPercent: number;\n};\n\n// Tests\n\nexport type TestChoice = { id: string; label: string };\n\nexport type TestQuestionConfig =\n | { type: \"mcq\"; choices: TestChoice[]; multiple: boolean }\n | {\n type: \"todolist\";\n steps: { id: string; label: string; required: boolean }[];\n }\n | { type: \"fill_gap\"; template: string; blanks: string[] }\n | { type: \"sort_order\"; items: TestChoice[] };\n\nexport type TestQuestion = {\n id: string;\n prompt: string;\n points: number;\n config: TestQuestionConfig;\n};\n\nexport type QuestionAnswer =\n | { type: \"mcq\"; selectedIds: string[] }\n | { type: \"todolist\"; checkedIds: string[] }\n | { type: \"fill_gap\"; values: Record<string, string> }\n | { type: \"sort_order\"; orderedIds: string[] };\n\n/** Answers by question id. */\nexport type TestAnswers = Record<string, QuestionAnswer>;\n\nexport type Test = {\n id: string;\n title: string;\n description: string;\n gating: boolean;\n passThresholdPercent: number;\n /** 0 means unlimited. */\n attemptsAllowed: number;\n /** `undefined` when the attempts are unlimited. */\n attemptsLeft?: number;\n attemptCount: number;\n bestScorePercent: number;\n passed: boolean;\n questions: TestQuestion[];\n};\n\nexport type TestSubmission = {\n id: string;\n scorePercent: number;\n passed: boolean;\n durationMs: number;\n createdAt?: Date;\n};\n\nexport type QuestionResult = {\n questionId: string;\n correct: boolean;\n points: number;\n /** Only when the show-answers rule of the test allows it. */\n correctAnswer?: QuestionAnswer;\n};\n\nexport type TestResult = {\n submission: TestSubmission;\n results: QuestionResult[];\n attemptsLeft?: number;\n};\n\nexport type TestSubmissionList = {\n submissions: TestSubmission[];\n bestScorePercent: number;\n attemptCount: number;\n};\n\n// Questions and ratings\n\nexport type LessonQuestion = {\n id: string;\n content: string;\n authorName: string;\n isMine: boolean;\n createdAt?: Date;\n answer?: { content: string; createdAt?: Date };\n};\n\nexport type LessonRating = {\n myRating: number;\n averageRating: number;\n ratingCount: number;\n};\n\nexport type CourseErrorCode =\n | \"unauthorized\"\n | \"course_not_found\"\n | \"item_not_found\"\n | \"item_locked\"\n | \"no_attempts_left\"\n | \"invalid_argument\"\n | \"rate_limited\"\n | \"unavailable\"\n | \"unknown\";\n\n/** Failure reported by a data source. */\nexport class CourseError extends Error {\n readonly code: CourseErrorCode;\n\n constructor(code: CourseErrorCode, message?: string) {\n super(message ?? code);\n this.name = \"CourseError\";\n this.code = code;\n }\n}\n\n/**\n * Everything `PanelCourse` needs from a backend, scoped to one course and one\n * customer. Methods reject with `CourseError` for failures the view can\n * explain (any other error is shown as a generic one).\n *\n * The default implementation is `createRpcCourseDataSource`; the creator\n * preview of the Bitsnap dashboard passes its own.\n */\nexport interface CourseDataSource {\n getCourse(): Promise<Course>;\n getLesson(lessonId: string): Promise<Lesson>;\n saveProgress(input: SaveProgressInput): Promise<ProgressSaved>;\n getTest(testId: string): Promise<Test>;\n submitTest(\n testId: string,\n answers: TestAnswers,\n durationMs: number,\n ): Promise<TestResult>;\n listSubmissions(testId: string): Promise<TestSubmissionList>;\n listQuestions(lessonId: string): Promise<LessonQuestion[]>;\n askQuestion(lessonId: string, content: string): Promise<LessonQuestion>;\n rate(\n lessonId: string,\n score: number,\n comment?: string,\n ): Promise<LessonRating>;\n}\n","import { ChevronLeft, ChevronRight, Download, Paperclip } from \"lucide-react\";\nimport {\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n type ReactNode,\n} from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Button } from \"../ui/button\";\nimport { useCourseTrack } from \"./analytics\";\nimport { useCourseView } from \"./context\";\nimport {\n fillTemplate,\n formatBytes,\n isPlaybackComplete,\n resumePosition,\n} from \"./logic\";\nimport { LessonQuestions, LessonRatingBox } from \"./LessonSocial\";\nimport { sanitizeLessonHtml } from \"./sanitize\";\nimport type {\n Course,\n CourseItem,\n ItemRef,\n Lesson,\n ProgressSaved,\n} from \"./types\";\nimport { ACCENT_BUTTON, CARD } from \"./ui\";\nimport { VideoPlayer, type PlaybackReport } from \"./VideoPlayer\";\n\ntype LessonPaneProps = {\n lesson: Lesson;\n mobileOutline?: ReactNode;\n nextItem?: CourseItem;\n /** 1-based place of the lesson among the lessons of the course. */\n position?: number;\n courseType: Course[\"type\"];\n /** Previous / next item taken from the course structure. */\n prev?: ItemRef;\n next?: ItemRef;\n autoplay: boolean;\n onAutoplayChange: (value: boolean) => void;\n /** Start playing right away (the viewer came from autoplay). */\n startPlaying: boolean;\n onNavigate: (ref: ItemRef) => void;\n onProgressSaved: (lessonId: string, saved: ProgressSaved) => void;\n onEnded: () => void;\n /** Ask for a fresh lesson (signed URLs expired). */\n onUrlsExpired: () => void;\n};\n\n/**\n * The lesson: video, text, attachments, actions, rating and questions. The\n * owner gives it a `key` of the lesson id, so a lesson never reports the\n * progress of another one.\n */\nexport function LessonPane({\n lesson,\n mobileOutline,\n nextItem,\n position = 0,\n courseType,\n prev,\n next,\n autoplay,\n onAutoplayChange,\n startPlaying,\n onNavigate,\n onProgressSaved,\n onEnded,\n onUrlsExpired,\n}: LessonPaneProps) {\n const { dataSource, texts, preview } = useCourseView();\n const track = useCourseTrack();\n const [completed, setCompleted] = useState(lesson.completed);\n const completedRef = useRef(lesson.completed);\n const lastSavedPosition = useRef<number | undefined>(undefined);\n const lastKnownPosition = useRef(lesson.positionSeconds);\n const [saving, setSaving] = useState(false);\n const [ended, setEnded] = useState(false);\n const [announced, setAnnounced] = useState(false);\n const tabId = useId();\n const [activeTab, setActiveTab] = useState(\n courseType === \"video\" ? \"lessons\" : \"description\",\n );\n\n // The pane lives as long as one view of the lesson.\n useEffect(() => {\n track(\"lesson_started\", { lesson_id: lesson.id, position });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [lesson.id]);\n\n // The signed URLs of a long-open lesson expire; refresh them before then.\n const expiresAt = lesson.urlsExpireAt?.getTime();\n useEffect(() => {\n if (expiresAt == null) return;\n const delay = expiresAt - Date.now() - 60_000;\n if (delay <= 0 || delay > 2 ** 31 - 1) return;\n const timer = setTimeout(onUrlsExpired, delay);\n return () => clearTimeout(timer);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [expiresAt]);\n\n // Sanitised in the browser; the server renders no lesson HTML.\n const html = useMemo(\n () => sanitizeLessonHtml(lesson.contentHtml),\n [lesson.contentHtml],\n );\n\n async function save(\n positionSeconds: number,\n durationSeconds: number,\n markCompleted: boolean,\n ) {\n if (preview) return;\n const saved = await dataSource.saveProgress({\n lessonId: lesson.id,\n completed: markCompleted,\n positionSeconds,\n durationSeconds,\n });\n lastSavedPosition.current = positionSeconds;\n if (saved.completed && !completedRef.current) {\n completedRef.current = true;\n setCompleted(true);\n setAnnounced(true);\n track(\"lesson_completed\", { lesson_id: lesson.id, position });\n }\n onProgressSaved(lesson.id, saved);\n }\n\n function handleReport(report: PlaybackReport) {\n lastKnownPosition.current = report.positionSeconds;\n if (preview) return;\n if (\n report.reason === \"periodic\" &&\n report.positionSeconds === lastSavedPosition.current\n ) {\n return;\n }\n const reachedEnd =\n report.reason === \"ended\" ||\n isPlaybackComplete(report.positionSeconds, report.durationSeconds);\n save(\n report.positionSeconds,\n report.durationSeconds,\n reachedEnd && !completedRef.current,\n ).catch((error: unknown) => {\n // Progress is saved again on the next tick; it must not interrupt the video.\n console.warn(\"Saving the progress of the lesson failed\", error);\n });\n }\n\n async function markComplete() {\n setSaving(true);\n try {\n await save(\n lastKnownPosition.current,\n lesson.video?.durationSeconds ?? 0,\n true,\n );\n } catch (error) {\n console.warn(\"Marking the lesson as completed failed\", error);\n } finally {\n setSaving(false);\n }\n }\n\n const description = lesson.description.trim();\n const hasBody = html !== \"\" || description !== \"\";\n\n const tabs = [\n ...(courseType === \"video\" && mobileOutline\n ? [{ id: \"lessons\", label: texts.lessonsTab ?? texts.contents }]\n : []),\n { id: \"description\", label: texts.descriptionTab ?? lesson.title },\n ...(lesson.attachments.length\n ? [{ id: \"files\", label: texts.filesTab ?? texts.attachments }]\n : []),\n { id: \"questions\", label: texts.questionsTab ?? texts.questionsTitle },\n ];\n const selectedTab = tabs.some((tab) => tab.id === activeTab)\n ? activeTab\n : tabs[0].id;\n const panelClass = (id: string) =>\n cn(selectedTab !== id && \"hidden\", \"lg:block\");\n const panelProps = (id: string) => ({\n id: `${tabId}-panel-${id}`,\n role: \"tabpanel\" as const,\n \"aria-labelledby\": `${tabId}-tab-${id}`,\n tabIndex: 0,\n });\n const nextLocked = nextItem?.locked ?? false;\n const completionButton = !preview && (\n <Button\n size=\"sm\"\n variant={completed ? \"outline\" : \"default\"}\n className={cn(\n \"min-h-11 h-auto whitespace-normal\",\n !completed && ACCENT_BUTTON,\n )}\n disabled={completed || saving}\n onClick={markComplete}\n >\n {completed ? texts.markedComplete : texts.markComplete}\n </Button>\n );\n const navigation = (mobile: boolean) => (\n <nav\n aria-label={texts.lessonNavigation ?? texts.contents}\n data-lesson-navigation={mobile ? \"mobile\" : \"desktop\"}\n className={\n mobile\n ? \"fixed inset-x-0 bottom-[var(--course-mobile-bottom,0px)] z-40 flex items-center justify-between gap-2 border-t border-neutral-200 bg-white px-3 pt-2 pb-[max(0.5rem,env(safe-area-inset-bottom))] dark:border-neutral-700 dark:bg-neutral-900 lg:hidden\"\n : \"hidden items-center justify-between gap-3 lg:flex\"\n }\n >\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"min-h-11\"\n disabled={prev == null}\n onClick={() => prev && onNavigate(prev)}\n >\n <ChevronLeft aria-hidden />\n {mobile ? (texts.mobilePrev ?? texts.prevItem) : texts.prevItem}\n </Button>\n {completionButton}\n <Button\n variant=\"outline\"\n size=\"sm\"\n className=\"min-h-11\"\n disabled={next == null || nextLocked}\n onClick={() => next && !nextLocked && onNavigate(next)}\n >\n {mobile ? (texts.mobileNext ?? texts.nextItem) : texts.nextItem}\n <ChevronRight aria-hidden />\n </Button>\n </nav>\n );\n\n return (\n <article\n data-lesson-id={lesson.id}\n className=\"min-w-0 space-y-5 pb-[calc(5rem+env(safe-area-inset-bottom)+var(--course-mobile-bottom,0px))] lg:pb-0\"\n aria-labelledby={`lesson-${lesson.id}`}\n >\n {lesson.video && (\n <VideoPlayer\n src={lesson.video.hlsUrl}\n poster={lesson.video.posterUrl}\n lessonId={lesson.id}\n startAt={resumePosition(\n lastKnownPosition.current,\n lesson.video.durationSeconds,\n )}\n durationHint={lesson.video.durationSeconds}\n autoPlay={startPlaying}\n onReport={handleReport}\n onEnded={() => {\n setEnded(true);\n onEnded();\n }}\n onExpired={onUrlsExpired}\n />\n )}\n\n {navigation(false)}\n <div className=\"lg:hidden\" role=\"tablist\" aria-label={texts.contents}>\n <div className=\"flex overflow-x-auto border-b border-neutral-200 dark:border-neutral-700\">\n {tabs.map((tab, index) => (\n <button\n key={tab.id}\n type=\"button\"\n role=\"tab\"\n id={`${tabId}-tab-${tab.id}`}\n aria-selected={selectedTab === tab.id}\n aria-controls={`${tabId}-panel-${tab.id}`}\n tabIndex={selectedTab === tab.id ? 0 : -1}\n className={cn(\n \"min-h-11 min-w-11 shrink-0 border-b-2 px-4 text-sm font-medium focus-visible:outline focus-visible:outline-2\",\n selectedTab === tab.id\n ? \"border-current\"\n : \"border-transparent text-neutral-600 dark:text-neutral-300\",\n )}\n onClick={() => setActiveTab(tab.id)}\n onKeyDown={(event) => {\n const offset =\n event.key === \"ArrowRight\"\n ? 1\n : event.key === \"ArrowLeft\"\n ? -1\n : 0;\n if (!offset && event.key !== \"Home\" && event.key !== \"End\")\n return;\n event.preventDefault();\n const target =\n event.key === \"Home\"\n ? 0\n : event.key === \"End\"\n ? tabs.length - 1\n : (index + offset + tabs.length) % tabs.length;\n setActiveTab(tabs[target].id);\n event.currentTarget.parentElement\n ?.querySelectorAll<HTMLButtonElement>('[role=\"tab\"]')\n [target]?.focus();\n }}\n >\n {tab.label}\n </button>\n ))}\n </div>\n </div>\n {courseType === \"video\" && mobileOutline && (\n <div\n {...panelProps(\"lessons\")}\n className={cn(\n CARD,\n selectedTab !== \"lessons\" && \"hidden\",\n \"lg:hidden\",\n )}\n >\n {mobileOutline}\n </div>\n )}\n <div className={cn(CARD, \"space-y-5\")}>\n <header className=\"flex flex-wrap items-start justify-between gap-3\">\n <h2\n id={`lesson-${lesson.id}`}\n className=\"text-2xl font-semibold break-words\"\n >\n {lesson.title}\n </h2>\n </header>\n {courseType === \"video\" && lesson.video && (\n <label className=\"flex min-h-11 w-fit cursor-pointer items-center gap-2 text-sm text-neutral-600 dark:text-neutral-300\">\n <input\n type=\"checkbox\"\n checked={autoplay}\n onChange={(e) => onAutoplayChange(e.target.checked)}\n />\n {texts.autoplay}\n </label>\n )}\n <div\n {...panelProps(\"description\")}\n className={panelClass(\"description\")}\n >\n {description !== \"\" && (\n <p className=\"text-sm break-words whitespace-pre-wrap text-neutral-700 dark:text-neutral-300\">\n {description}\n </p>\n )}\n\n {html !== \"\" && (\n <div\n className=\"bitsnap-course-html break-words [&_a]:underline [&_blockquote]:border-l-4 [&_blockquote]:pl-4 [&_h1]:text-2xl [&_h1]:font-semibold [&_h2]:text-xl [&_h2]:font-semibold [&_h3]:text-lg [&_h3]:font-semibold [&_img]:h-auto [&_img]:max-w-full [&_ol]:list-decimal [&_ol]:pl-6 [&_p]:my-3 [&_pre]:overflow-x-auto [&_table]:block [&_table]:max-w-full [&_table]:overflow-x-auto [&_ul]:list-disc [&_ul]:pl-6\"\n // Sanitised by DOMPurify (see sanitize.ts).\n dangerouslySetInnerHTML={{ __html: html }}\n />\n )}\n\n {!hasBody && !lesson.video && (\n <p className=\"text-sm text-neutral-500 dark:text-neutral-400\">\n {texts.noContent}\n </p>\n )}\n </div>\n {lesson.attachments.length > 0 && (\n <div {...panelProps(\"files\")} className={panelClass(\"files\")}>\n {lesson.attachments.length > 0 && (\n <section\n aria-labelledby={`files-${lesson.id}`}\n className=\"space-y-2\"\n >\n <h3\n id={`files-${lesson.id}`}\n className=\"flex items-center gap-2 text-lg font-semibold\"\n >\n <Paperclip className=\"size-4\" aria-hidden />\n {texts.attachments}\n </h3>\n <ul className=\"space-y-2\">\n {lesson.attachments.map((file) => (\n <li key={file.id}>\n <a\n href={file.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n download={file.name}\n className=\"flex min-h-11 items-center justify-between gap-3 rounded-lg bg-neutral-100 px-3 py-2 text-sm transition hover:bg-neutral-200 dark:bg-neutral-800 dark:hover:bg-neutral-700\"\n >\n <span className=\"min-w-0 break-words\">\n {file.name}\n <span className=\"ml-2 text-xs text-neutral-500 dark:text-neutral-400\">\n {formatBytes(file.size)}\n </span>\n </span>\n <span className=\"flex shrink-0 items-center gap-1 text-xs font-medium\">\n {texts.download}\n <Download className=\"size-3.5\" aria-hidden />\n </span>\n </a>\n </li>\n ))}\n </ul>\n </section>\n )}\n </div>\n )}\n </div>\n\n <div className={cn(CARD, panelClass(\"description\"))}>\n <LessonRatingBox\n lessonId={lesson.id}\n initial={{\n myRating: lesson.myRating,\n averageRating: lesson.averageRating,\n ratingCount: lesson.ratingCount,\n }}\n />\n </div>\n\n <div\n {...panelProps(\"questions\")}\n className={cn(CARD, panelClass(\"questions\"))}\n >\n <LessonQuestions lessonId={lesson.id} />\n </div>\n <p\n role=\"status\"\n aria-live=\"polite\"\n className=\"text-sm text-emerald-700 dark:text-emerald-400\"\n >\n {announced ? texts.markedComplete : \"\"}\n </p>\n {(completed || ended) && next && nextItem && (\n <section data-course-up-next className={cn(CARD, \"space-y-3\")}>\n <h3 className=\"font-semibold break-words\">\n {fillTemplate(texts.upNext ?? `${texts.nextItem}: {title}`, {\n title: nextItem.title,\n })}\n </h3>\n {nextLocked && (\n <p className=\"text-sm text-neutral-600 dark:text-neutral-300\">\n {nextItem.lockReason\n ? fillTemplate(texts.lockedByTest, {\n test: nextItem.lockReason.gatingTestTitle,\n })\n : texts.locked}\n </p>\n )}\n <Button\n className={cn(ACCENT_BUTTON, \"min-h-11\")}\n disabled={nextLocked}\n onClick={() => !nextLocked && onNavigate(next)}\n >\n {texts.openNext ?? texts.nextItem}\n </Button>\n </section>\n )}\n {navigation(true)}\n </article>\n );\n}\n","import { useState } from \"react\";\nimport { Button } from \"../ui/button\";\nimport { Textarea } from \"../ui/textarea\";\nimport { useCourseTrack } from \"./analytics\";\nimport { useCourseView } from \"./context\";\nimport { fillTemplate } from \"./logic\";\nimport { RatingStars } from \"./RatingStars\";\nimport type { LessonQuestion, LessonRating } from \"./types\";\nimport { ACCENT_BUTTON, ErrorBox, errorMessage } from \"./ui\";\nimport { useAsyncResource } from \"./useAsyncResource\";\n\n/** Star rating of a lesson with an optional comment. */\nexport function LessonRatingBox({\n lessonId,\n initial,\n}: {\n lessonId: string;\n initial: LessonRating;\n}) {\n const { dataSource, texts, preview } = useCourseView();\n const track = useCourseTrack();\n const [rating, setRating] = useState(initial);\n const [score, setScore] = useState(initial.myRating);\n const [comment, setComment] = useState(\"\");\n const [state, setState] = useState<\"idle\" | \"saving\" | \"saved\" | \"error\">(\n \"idle\",\n );\n const [error, setError] = useState<unknown>();\n\n async function save() {\n if (score < 1) return;\n setState(\"saving\");\n try {\n setRating(await dataSource.rate(lessonId, score, comment.trim()));\n setState(\"saved\");\n track(\"lesson_rated\", { lesson_id: lessonId, rating: score });\n } catch (e) {\n setError(e);\n setState(\"error\");\n }\n }\n\n return (\n <section aria-labelledby={`rate-${lessonId}`} className=\"space-y-3\">\n <h3 id={`rate-${lessonId}`} className=\"text-lg font-semibold\">\n {texts.rateTitle}\n </h3>\n <RatingStars\n value={score}\n onSelect={(n) => {\n setScore(n);\n setState(\"idle\");\n }}\n disabled={preview || state === \"saving\"}\n />\n {rating.ratingCount > 0 && (\n <p className=\"text-xs text-neutral-500 dark:text-neutral-400\">\n {fillTemplate(texts.rateAverage, {\n average: rating.averageRating.toFixed(1),\n count: rating.ratingCount,\n })}\n </p>\n )}\n {!preview && score > 0 && (\n <div className=\"space-y-2\">\n <Textarea\n value={comment}\n maxLength={1000}\n placeholder={texts.ratePlaceholder}\n onChange={(e) => {\n setComment(e.target.value);\n setState(\"idle\");\n }}\n />\n <Button\n size=\"sm\"\n className={ACCENT_BUTTON}\n disabled={state === \"saving\"}\n onClick={save}\n >\n {texts.rateSubmit}\n </Button>\n </div>\n )}\n {state === \"saved\" && (\n <p role=\"status\" className=\"text-sm text-emerald-700 dark:text-emerald-400\">\n {texts.rateSaved}\n </p>\n )}\n {state === \"error\" && (\n <p role=\"alert\" className=\"text-sm text-red-600 dark:text-red-400\">\n {errorMessage(error, texts)}\n </p>\n )}\n </section>\n );\n}\n\nfunction formatDate(date: Date | undefined): string {\n return date == null ? \"\" : date.toLocaleDateString();\n}\n\n/** Questions of the students about a lesson with the ask form. */\nexport function LessonQuestions({ lessonId }: { lessonId: string }) {\n const { dataSource, texts, preview } = useCourseView();\n const questions = useAsyncResource<LessonQuestion[]>(\n () => dataSource.listQuestions(lessonId),\n [dataSource, lessonId],\n );\n const [content, setContent] = useState(\"\");\n const [state, setState] = useState<\"idle\" | \"sending\" | \"sent\" | \"error\">(\n \"idle\",\n );\n const [error, setError] = useState<unknown>();\n\n async function ask() {\n const text = content.trim();\n if (text === \"\") return;\n setState(\"sending\");\n try {\n const created = await dataSource.askQuestion(lessonId, text);\n questions.mutate((list) => [created, ...list]);\n setContent(\"\");\n setState(\"sent\");\n } catch (e) {\n setError(e);\n setState(\"error\");\n }\n }\n\n return (\n <section aria-labelledby={`qa-${lessonId}`} className=\"space-y-4\">\n <h3 id={`qa-${lessonId}`} className=\"text-lg font-semibold\">\n {texts.questionsTitle}\n </h3>\n\n <form\n className=\"space-y-2\"\n onSubmit={(e) => {\n e.preventDefault();\n void ask();\n }}\n >\n <Textarea\n value={content}\n maxLength={2000}\n placeholder={texts.questionPlaceholder}\n aria-label={texts.questionPlaceholder}\n onChange={(e) => {\n setContent(e.target.value);\n setState(\"idle\");\n }}\n />\n <Button\n type=\"submit\"\n size=\"sm\"\n className={ACCENT_BUTTON}\n disabled={state === \"sending\" || content.trim() === \"\"}\n >\n {texts.questionSubmit}\n </Button>\n {state === \"sent\" && (\n <p role=\"status\" className=\"text-sm text-emerald-700 dark:text-emerald-400\">\n {texts.questionSent}\n </p>\n )}\n {state === \"error\" && (\n <p role=\"alert\" className=\"text-sm text-red-600 dark:text-red-400\">\n {errorMessage(error, texts)}\n </p>\n )}\n </form>\n\n {questions.status === \"loading\" && (\n <p role=\"status\" className=\"text-sm text-neutral-500\">\n {texts.loading}\n </p>\n )}\n {questions.status === \"error\" && (\n <ErrorBox error={questions.error} onRetry={questions.reload} />\n )}\n {questions.status === \"success\" &&\n (questions.data.length === 0 ? (\n <p className=\"text-sm text-neutral-500 dark:text-neutral-400\">\n {texts.questionsEmpty}\n </p>\n ) : (\n <ul className=\"space-y-3\">\n {questions.data.map((question) => (\n <li\n key={question.id}\n className=\"rounded-lg bg-neutral-100 p-3 text-sm dark:bg-neutral-800\"\n >\n <p className=\"mb-1 text-xs text-neutral-500 dark:text-neutral-400\">\n {question.isMine ? texts.questionMine : question.authorName}\n {question.createdAt && ` · ${formatDate(question.createdAt)}`}\n </p>\n <p className=\"break-words whitespace-pre-wrap\">{question.content}</p>\n {question.answer && (\n <div className=\"mt-2 border-l-2 border-neutral-400 pl-3 dark:border-neutral-500\">\n <p className=\"mb-1 text-xs font-medium text-neutral-500 dark:text-neutral-400\">\n {texts.questionTeamAnswer}\n {question.answer.createdAt &&\n ` · ${formatDate(question.answer.createdAt)}`}\n </p>\n <p className=\"break-words whitespace-pre-wrap\">\n {question.answer.content}\n </p>\n </div>\n )}\n </li>\n ))}\n </ul>\n ))}\n </section>\n );\n}\n","import { Star } from \"lucide-react\";\nimport { useState } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { useCourseView } from \"./context\";\nimport { fillTemplate } from \"./logic\";\n\n/** Five stars; read-only without `onSelect`. */\nexport function RatingStars({\n value,\n onSelect,\n disabled,\n}: {\n value: number;\n onSelect?: (score: number) => void;\n disabled?: boolean;\n}) {\n const { texts } = useCourseView();\n const [hover, setHover] = useState(0);\n const shown = hover || value;\n\n return (\n <div\n className=\"flex items-center gap-1\"\n role={onSelect ? \"radiogroup\" : \"img\"}\n aria-label={fillTemplate(texts.rateStar, { n: Math.round(value) })}\n >\n {[1, 2, 3, 4, 5].map((n) => {\n const filled = n <= Math.round(shown);\n const star = (\n <Star\n className={cn(\n \"size-5\",\n filled\n ? \"fill-amber-400 text-amber-400\"\n : \"text-neutral-300 dark:text-neutral-600\",\n )}\n aria-hidden\n />\n );\n return onSelect ? (\n <button\n key={n}\n type=\"button\"\n role=\"radio\"\n aria-checked={n === value}\n aria-label={fillTemplate(texts.rateStar, { n })}\n disabled={disabled}\n className=\"rounded p-0.5 outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:opacity-50\"\n onMouseEnter={() => setHover(n)}\n onMouseLeave={() => setHover(0)}\n onClick={() => onSelect(n)}\n >\n {star}\n </button>\n ) : (\n <span key={n}>{star}</span>\n );\n })}\n </div>\n );\n}\n","import { AlertTriangle } from \"lucide-react\";\nimport { cn } from \"../lib/utils\";\nimport { Button } from \"../ui/button\";\nimport { Skeleton } from \"../ui/skeleton\";\nimport { useCourseView } from \"./context\";\nimport type { CourseTexts } from \"./texts\";\nimport { CourseError } from \"./types\";\n\n/** Accent (brand) colors of the course, set as CSS variables by `PanelCourse`. */\nexport const ACCENT_BG =\n \"bg-[var(--course-accent,var(--primary))] text-[var(--course-accent-fg,var(--primary-foreground))] dark:bg-[var(--course-accent-dark,var(--course-accent,var(--primary)))] dark:text-[var(--course-accent-dark-fg,var(--course-accent-fg,var(--primary-foreground)))]\";\n\nexport const ACCENT_BUTTON = cn(\n ACCENT_BG,\n \"hover:bg-[var(--course-accent,var(--primary))] hover:opacity-90 dark:hover:bg-[var(--course-accent-dark,var(--course-accent,var(--primary)))]\",\n);\n\nexport const CARD =\n \"rounded-xl bg-white p-4 drop-shadow-xl sm:p-6 dark:border dark:border-neutral-700 dark:bg-neutral-900 dark:drop-shadow-none\";\n\n/** The explanation of an error that the view can show to the customer. */\nexport function errorMessage(error: unknown, texts: CourseTexts): string {\n if (error instanceof CourseError) {\n switch (error.code) {\n case \"unauthorized\":\n return texts.errorUnauthorized;\n case \"course_not_found\":\n case \"item_not_found\":\n return texts.errorNotFound;\n case \"unavailable\":\n return texts.errorUnavailable;\n }\n }\n return texts.errorGeneric;\n}\n\nexport function ErrorBox({\n error,\n onRetry,\n className,\n}: {\n error: unknown;\n onRetry?: () => void;\n className?: string;\n}) {\n const { texts } = useCourseView();\n return (\n <div\n role=\"alert\"\n className={cn(\n \"flex flex-col items-start gap-3 rounded-xl border border-red-200 bg-red-50 p-4 text-sm text-red-700 dark:border-red-900 dark:bg-red-950 dark:text-red-300\",\n className,\n )}\n >\n <p className=\"flex items-center gap-2 font-medium\">\n <AlertTriangle className=\"size-4 shrink-0\" aria-hidden />\n {errorMessage(error, texts)}\n </p>\n {onRetry && (\n <Button variant=\"outline\" size=\"sm\" onClick={onRetry}>\n {texts.retry}\n </Button>\n )}\n </div>\n );\n}\n\nexport function LoadingBlock({ label }: { label?: string }) {\n const { texts } = useCourseView();\n return (\n <div role=\"status\" aria-label={label ?? texts.loading} className=\"space-y-3\">\n <Skeleton className=\"aspect-video w-full\" />\n <Skeleton className=\"h-6 w-2/3\" />\n <Skeleton className=\"h-4 w-full\" />\n <Skeleton className=\"h-4 w-5/6\" />\n </div>\n );\n}\n\nexport function ProgressBar({\n percent,\n label,\n}: {\n percent: number;\n label: string;\n}) {\n const value = Math.max(0, Math.min(100, Math.round(percent)));\n return (\n <div\n role=\"progressbar\"\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={value}\n aria-label={label}\n className=\"h-2 w-full overflow-hidden rounded-full bg-neutral-200 dark:bg-neutral-700\"\n >\n <div\n className={cn(\"h-full rounded-full transition-all duration-300\", ACCENT_BG)}\n style={{ width: `${value}%` }}\n />\n </div>\n );\n}\n","import { useCallback, useEffect, useRef, useState } from \"react\";\n\nexport type AsyncResource<T> =\n | { status: \"loading\"; data?: undefined; error?: undefined }\n | { status: \"success\"; data: T; error?: undefined }\n | { status: \"error\"; data?: undefined; error: unknown };\n\n/**\n * Loads a value on the client when `deps` change. `reload` shows the loading\n * state again, `refresh` replaces the data quietly, `mutate` edits it locally.\n * An answer to an outdated request is dropped.\n */\nexport function useAsyncResource<T>(\n load: () => Promise<T>,\n deps: readonly unknown[],\n initialData?: T,\n): AsyncResource<T> & {\n reload: () => void;\n refresh: () => void;\n mutate: (update: (data: T) => T) => void;\n} {\n const [state, setState] = useState<AsyncResource<T>>(\n initialData !== undefined\n ? { status: \"success\", data: initialData }\n : { status: \"loading\" },\n );\n const [reloadKey, setReloadKey] = useState(0);\n const requestId = useRef(0);\n const loadRef = useRef(load);\n loadRef.current = load;\n const skipFirst = useRef(initialData !== undefined);\n\n useEffect(() => {\n if (skipFirst.current) {\n skipFirst.current = false;\n return;\n }\n const id = ++requestId.current;\n setState({ status: \"loading\" });\n loadRef\n .current()\n .then((data) => {\n if (id === requestId.current) setState({ status: \"success\", data });\n })\n .catch((error: unknown) => {\n if (id === requestId.current) setState({ status: \"error\", error });\n });\n return () => {\n requestId.current += 1;\n };\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [...deps, reloadKey]);\n\n const reload = useCallback(() => setReloadKey((key) => key + 1), []);\n\n const refresh = useCallback(() => {\n const id = ++requestId.current;\n loadRef\n .current()\n .then((data) => {\n if (id === requestId.current) setState({ status: \"success\", data });\n })\n .catch(() => {\n // Keep what is on screen; a quiet refresh must not replace it with an error.\n });\n }, []);\n\n const mutate = useCallback((update: (data: T) => T) => {\n setState((current) =>\n current.status === \"success\"\n ? { status: \"success\", data: update(current.data) }\n : current,\n );\n }, []);\n\n return { ...state, reload, refresh, mutate } as AsyncResource<T> & {\n reload: () => void;\n refresh: () => void;\n mutate: (update: (data: T) => T) => void;\n };\n}\n","import DOMPurify from \"dompurify\";\n\n/** The part of DOMPurify the wrapper uses, so tests can bring their own DOM. */\nexport type PurifyLike = {\n isSupported?: boolean;\n sanitize?: (html: string, config: Record<string, unknown>) => unknown;\n addHook?: (\n name: \"afterSanitizeAttributes\",\n hook: (node: Element) => void,\n ) => void;\n};\n\nconst SANITIZE_CONFIG = {\n USE_PROFILES: { html: true },\n // Lesson text is content, not a page: no embedded documents, no forms and\n // no inline styles (they allow overlays on top of the panel).\n FORBID_TAGS: [\n \"style\",\n \"form\",\n \"input\",\n \"button\",\n \"textarea\",\n \"select\",\n \"iframe\",\n \"frame\",\n \"object\",\n \"embed\",\n \"base\",\n \"meta\",\n \"link\",\n ],\n FORBID_ATTR: [\"style\", \"srcdoc\", \"formaction\"],\n ALLOW_DATA_ATTR: false,\n RETURN_TRUSTED_TYPE: false,\n};\n\n/**\n * Builds a sanitizer around a DOMPurify instance. Links open in a new tab\n * without `opener`, images load lazily. Without a DOM (server rendering) the\n * sanitizer returns an empty string rather than unsanitised HTML.\n */\nexport function createHtmlSanitizer(\n purify: PurifyLike,\n): (html: string | null | undefined) => string {\n if (purify.isSupported === false || purify.sanitize == null) {\n return () => \"\";\n }\n const sanitize = purify.sanitize.bind(purify);\n\n purify.addHook?.(\"afterSanitizeAttributes\", (node) => {\n if (node.tagName === \"A\" && node.hasAttribute(\"href\")) {\n node.setAttribute(\"target\", \"_blank\");\n node.setAttribute(\"rel\", \"noopener noreferrer nofollow\");\n }\n if (node.tagName === \"IMG\") {\n node.setAttribute(\"loading\", \"lazy\");\n node.setAttribute(\"referrerpolicy\", \"no-referrer\");\n }\n });\n\n return (html) => {\n if (html == null || html === \"\") {\n return \"\";\n }\n return String(sanitize(html, SANITIZE_CONFIG));\n };\n}\n\nlet sharedSanitizer: ((html: string | null | undefined) => string) | undefined;\n\n/**\n * Sanitizes the HTML of a lesson with the DOM of the browser. Returns an empty\n * string when there is no DOM, so call it on the client (inside an effect or\n * `useMemo` of a component that only renders content after loading).\n */\nexport function sanitizeLessonHtml(html: string | null | undefined): string {\n if (typeof window === \"undefined\") {\n return \"\";\n }\n sharedSanitizer ??= createHtmlSanitizer(DOMPurify as unknown as PurifyLike);\n return sharedSanitizer(html);\n}\n","import Hls from \"hls.js\";\nimport { useCallback, useEffect, useRef, useState } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Button } from \"../ui/button\";\nimport { newMilestones } from \"@/src/analytics/instrument\";\nimport { useCourseTrack } from \"./analytics\";\nimport { useCourseView } from \"./context\";\nimport { PROGRESS_SAVE_INTERVAL_MS } from \"./logic\";\n\nexport type PlaybackReport = {\n positionSeconds: number;\n durationSeconds: number;\n reason: \"periodic\" | \"pause\" | \"ended\" | \"leave\";\n};\n\ntype VideoPlayerProps = {\n /** HLS manifest URL. */\n src: string;\n poster?: string;\n /** Position to resume from, seconds. */\n startAt?: number;\n /** Length known from the lesson, used until the player knows it. */\n durationHint?: number;\n autoPlay?: boolean;\n /** Lesson the video belongs to; enables the `video_progress` statistics. */\n lessonId?: string;\n /** Reports the position every ~15 s while playing, on pause and at the end. */\n onReport?: (report: PlaybackReport) => void;\n onEnded?: () => void;\n /** The signed URL was refused; the owner should load a fresh one. */\n onExpired?: () => void;\n className?: string;\n};\n\nfunction canPlayNativeHls(video: HTMLVideoElement): boolean {\n return video.canPlayType(\"application/vnd.apple.mpegurl\") !== \"\";\n}\n\n/**\n * HLS player: hls.js where there is Media Source support, the native player\n * of Safari otherwise.\n */\nexport function VideoPlayer({\n src,\n poster,\n startAt = 0,\n durationHint = 0,\n autoPlay = false,\n lessonId,\n onReport,\n onEnded,\n onExpired,\n className,\n}: VideoPlayerProps) {\n const { texts } = useCourseView();\n const track = useCourseTrack();\n const trackRef = useRef(track);\n trackRef.current = track;\n const videoRef = useRef<HTMLVideoElement>(null);\n const [failed, setFailed] = useState(false);\n const [attempt, setAttempt] = useState(0);\n\n // Callbacks change on every render of the owner; the player must not reload.\n const callbacks = useRef({ onReport, onEnded, onExpired, durationHint });\n callbacks.current = { onReport, onEnded, onExpired, durationHint };\n const lastReported = useRef<number | undefined>(undefined);\n\n // React clears `videoRef` before the effect cleanup runs, so the cleanup\n // passes the element it captured.\n const report = useCallback((reason: PlaybackReport[\"reason\"], video = videoRef.current) => {\n if (video == null || callbacks.current.onReport == null) {\n return;\n }\n const positionSeconds = Math.floor(video.currentTime);\n if (positionSeconds <= 0 && reason !== \"ended\") {\n return;\n }\n const duration = Number.isFinite(video.duration)\n ? video.duration\n : callbacks.current.durationHint;\n lastReported.current = positionSeconds;\n callbacks.current.onReport({\n positionSeconds,\n durationSeconds: Math.floor(duration || 0),\n reason,\n });\n }, []);\n\n useEffect(() => {\n const video = videoRef.current;\n if (video == null) {\n return;\n }\n setFailed(false);\n let hls: Hls | undefined;\n let retriedNetwork = false;\n let retriedMedia = false;\n let timer: ReturnType<typeof setInterval> | undefined;\n\n const start = () => {\n if (startAt > 0) {\n video.currentTime = startAt;\n }\n if (autoPlay) {\n void video.play().catch(() => {\n // The browser blocked autoplay; the viewer presses play.\n });\n }\n };\n\n if (canPlayNativeHls(video)) {\n video.src = src;\n video.addEventListener(\"loadedmetadata\", start, { once: true });\n } else if (Hls.isSupported()) {\n hls = new Hls();\n hls.attachMedia(video);\n hls.loadSource(src);\n hls.on(Hls.Events.MANIFEST_PARSED, start);\n hls.on(Hls.Events.ERROR, (_event, data) => {\n if (!data.fatal) {\n return;\n }\n const status = data.response?.code;\n if (status === 401 || status === 403) {\n callbacks.current.onExpired?.();\n setFailed(true);\n } else if (data.type === Hls.ErrorTypes.NETWORK_ERROR && !retriedNetwork) {\n retriedNetwork = true;\n hls?.startLoad();\n } else if (data.type === Hls.ErrorTypes.MEDIA_ERROR && !retriedMedia) {\n retriedMedia = true;\n hls?.recoverMediaError();\n } else {\n setFailed(true);\n }\n });\n } else {\n video.src = src;\n }\n\n const onPlay = () => {\n clearInterval(timer);\n timer = setInterval(() => report(\"periodic\"), PROGRESS_SAVE_INTERVAL_MS);\n };\n const onPause = () => {\n clearInterval(timer);\n // `ended` has its own report.\n if (!video.ended) report(\"pause\");\n };\n // Milestones count only what was played through: a jump forward does not\n // report the parts that were skipped, and each one is sent once per view.\n const milestones = new Set<number>();\n let lastTime = video.currentTime;\n let watchedTo = 0;\n const sendMilestones = (percent: number) => {\n if (lessonId == null) {\n return;\n }\n for (const milestone of newMilestones(percent, milestones)) {\n trackRef.current(\"video_progress\", { lesson_id: lessonId, milestone });\n }\n };\n const onTimeUpdate = () => {\n const now = video.currentTime;\n const duration = Number.isFinite(video.duration)\n ? video.duration\n : callbacks.current.durationHint;\n if (duration > 0 && !video.seeking && now >= lastTime && now - lastTime <= 2) {\n watchedTo = Math.max(watchedTo, now);\n sendMilestones((watchedTo / duration) * 100);\n }\n lastTime = now;\n };\n const onVideoEnded = () => {\n clearInterval(timer);\n // Reaching the end counts even after a jump, but only as 100.\n if (lessonId != null && !milestones.has(100)) {\n milestones.add(100);\n trackRef.current(\"video_progress\", { lesson_id: lessonId, milestone: 100 });\n }\n report(\"ended\");\n callbacks.current.onEnded?.();\n };\n const onVideoError = () => {\n if (hls == null) {\n setFailed(true);\n }\n };\n\n video.addEventListener(\"play\", onPlay);\n video.addEventListener(\"pause\", onPause);\n video.addEventListener(\"ended\", onVideoEnded);\n video.addEventListener(\"error\", onVideoError);\n video.addEventListener(\"timeupdate\", onTimeUpdate);\n\n return () => {\n clearInterval(timer);\n video.removeEventListener(\"play\", onPlay);\n video.removeEventListener(\"pause\", onPause);\n video.removeEventListener(\"ended\", onVideoEnded);\n video.removeEventListener(\"error\", onVideoError);\n video.removeEventListener(\"timeupdate\", onTimeUpdate);\n video.removeEventListener(\"loadedmetadata\", start);\n // Leaving the lesson mid-video keeps the position.\n if (!video.ended && video.currentTime > 0) report(\"leave\", video);\n hls?.destroy();\n video.removeAttribute(\"src\");\n video.load();\n };\n // `startAt` and `autoPlay` only matter when a video starts loading.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [src, attempt]);\n\n return (\n <div className={cn(\"relative overflow-hidden rounded-xl bg-black\", className)}>\n <video\n ref={videoRef}\n className=\"aspect-video w-full\"\n poster={poster}\n controls\n playsInline\n preload=\"metadata\"\n controlsList=\"nodownload\"\n />\n {failed && (\n <div\n role=\"alert\"\n className=\"absolute inset-0 flex flex-col items-center justify-center gap-3 bg-black/80 p-4 text-center text-sm text-white\"\n >\n <p>{texts.videoError}</p>\n <Button\n variant=\"secondary\"\n size=\"sm\"\n onClick={() => {\n callbacks.current.onExpired?.();\n setAttempt((n) => n + 1);\n }}\n >\n {texts.videoRetry}\n </Button>\n </div>\n )}\n </div>\n );\n}\n","import { getLocale } from \"../components/checkout/locale\";\nimport { resolveTexts, type LocaleTables } from \"../lib/texts\";\n\n/**\n * Every string of the course view. Pass a partial object through the `texts`\n * prop of `PanelCourse` to override any of them, whatever the locale.\n * Placeholders like `{count}` stay in overrides.\n */\nexport type CourseTexts = {\n back: string;\n loading: string;\n retry: string;\n errorGeneric: string;\n errorUnauthorized: string;\n errorNotFound: string;\n errorUnavailable: string;\n previewBadge: string;\n /** Placeholders `{done}` and `{total}`. */\n progressLessons: string;\n /** Placeholder `{percent}`. */\n progressPercent: string;\n contents: string;\n free: string;\n locked: string;\n /** Placeholder `{test}`. */\n lockedByTest: string;\n /** Placeholder `{test}`. */\n lockedPage: string;\n testBadge: string;\n completed: string;\n continueCourse: string;\n noItem: string;\n // Optional additions preserve compatibility with existing complete text objects.\n lessonsTab?: string;\n descriptionTab?: string;\n filesTab?: string;\n questionsTab?: string;\n upNext?: string;\n openNext?: string;\n courseCompleted?: string;\n lessonNavigation?: string;\n mobilePrev?: string;\n mobileNext?: string;\n // Lesson\n markComplete: string;\n markedComplete: string;\n prevItem: string;\n nextItem: string;\n attachments: string;\n download: string;\n autoplay: string;\n videoError: string;\n videoRetry: string;\n noContent: string;\n // Test\n testGating: string;\n testAttemptsLeft: string;\n testAttemptsUnlimited: string;\n testNoAttemptsLeft: string;\n testPassThreshold: string;\n testBestScore: string;\n testSubmit: string;\n testSubmitting: string;\n /** Placeholder `{count}`. */\n testUnanswered: string;\n testPassed: string;\n testFailed: string;\n /** Placeholder `{percent}`. */\n testScore: string;\n testCorrect: string;\n testIncorrect: string;\n testCorrectAnswer: string;\n testTryAgain: string;\n testPreviousAttempts: string;\n /** Placeholders `{percent}` and `{date}`. */\n testAttemptRow: string;\n testNoQuestions: string;\n mcqSingle: string;\n mcqMultiple: string;\n todoHint: string;\n todoRequired: string;\n gapPlaceholder: string;\n sortHint: string;\n moveUp: string;\n moveDown: string;\n // Questions and ratings\n questionsTitle: string;\n questionsEmpty: string;\n questionPlaceholder: string;\n questionSubmit: string;\n questionSent: string;\n questionMine: string;\n questionTeamAnswer: string;\n rateTitle: string;\n /** Placeholder `{n}`. */\n rateStar: string;\n ratePlaceholder: string;\n rateSubmit: string;\n rateSaved: string;\n /** Placeholders `{average}` and `{count}`. */\n rateAverage: string;\n};\n\nexport const COURSE_TEXT_TABLES: LocaleTables<keyof CourseTexts> = {\n pl: {\n back: \"Wróć\",\n loading: \"Ładowanie kursu...\",\n retry: \"Spróbuj ponownie\",\n errorGeneric: \"Nie udało się załadować danych. Spróbuj ponownie za chwilę.\",\n errorUnauthorized:\n \"Nie masz dostępu do tego kursu. Zaloguj się ponownie lub skontaktuj się z pomocą.\",\n errorNotFound: \"Nie znaleziono tego kursu lub lekcji.\",\n errorUnavailable:\n \"Kurs jest chwilowo niedostępny. Spróbuj ponownie za chwilę.\",\n previewBadge: \"Podgląd\",\n progressLessons: \"{done} z {total} lekcji\",\n progressPercent: \"{percent}% ukończono\",\n contents: \"Spis treści\",\n free: \"Darmowa\",\n locked: \"Zablokowane\",\n lockedByTest: \"Odblokuje je zaliczenie testu: {test}\",\n lockedPage:\n \"Ta lekcja jest zablokowana. Zalicz test „{test}”, aby ją odblokować.\",\n testBadge: \"Test\",\n completed: \"Ukończono\",\n continueCourse: \"Kontynuuj kurs\",\n noItem: \"Wybierz lekcję z listy.\",\n lessonsTab: \"Lekcje\",\n descriptionTab: \"Opis\",\n filesTab: \"Pliki\",\n questionsTab: \"Pytania\",\n upNext: \"Następna lekcja: {title}\",\n openNext: \"Przejdź dalej\",\n courseCompleted: \"Kurs ukończony\",\n lessonNavigation: \"Nawigacja lekcji\",\n mobilePrev: \"Wstecz\",\n mobileNext: \"Dalej\",\n markComplete: \"Oznacz jako ukończoną\",\n markedComplete: \"Lekcja ukończona\",\n prevItem: \"Poprzednia\",\n nextItem: \"Następna\",\n attachments: \"Materiały do pobrania\",\n download: \"Pobierz\",\n autoplay: \"Autoodtwarzanie następnej lekcji\",\n videoError: \"Nie udało się odtworzyć wideo.\",\n videoRetry: \"Odśwież wideo\",\n noContent: \"Ta lekcja nie ma jeszcze treści.\",\n testGating: \"Zaliczenie tego testu odblokowuje kolejne lekcje.\",\n testAttemptsLeft: \"Pozostało prób: {count}\",\n testAttemptsUnlimited: \"Liczba prób nie jest ograniczona.\",\n testNoAttemptsLeft: \"Wykorzystano wszystkie próby.\",\n testPassThreshold: \"Próg zaliczenia: {percent}%\",\n testBestScore: \"Najlepszy wynik: {percent}%\",\n testSubmit: \"Sprawdź odpowiedzi\",\n testSubmitting: \"Sprawdzanie...\",\n testUnanswered: \"Pytania bez odpowiedzi: {count}\",\n testPassed: \"Test zaliczony\",\n testFailed: \"Test niezaliczony\",\n testScore: \"Wynik: {percent}%\",\n testCorrect: \"Poprawnie\",\n testIncorrect: \"Niepoprawnie\",\n testCorrectAnswer: \"Poprawna odpowiedź\",\n testTryAgain: \"Spróbuj ponownie\",\n testPreviousAttempts: \"Poprzednie podejścia\",\n testAttemptRow: \"{percent}% · {date}\",\n testNoQuestions: \"Ten test nie ma jeszcze pytań.\",\n mcqSingle: \"Wybierz jedną odpowiedź\",\n mcqMultiple: \"Zaznacz wszystkie poprawne odpowiedzi\",\n todoHint: \"Zaznacz wykonane kroki\",\n todoRequired: \"wymagany\",\n gapPlaceholder: \"uzupełnij\",\n sortHint: \"Ułóż elementy w poprawnej kolejności\",\n moveUp: \"Przesuń w górę\",\n moveDown: \"Przesuń w dół\",\n questionsTitle: \"Pytania do lekcji\",\n questionsEmpty: \"Nikt jeszcze nie zadał pytania.\",\n questionPlaceholder: \"Zadaj pytanie autorowi kursu\",\n questionSubmit: \"Wyślij pytanie\",\n questionSent: \"Pytanie wysłane.\",\n questionMine: \"Ty\",\n questionTeamAnswer: \"Odpowiedź autora\",\n rateTitle: \"Oceń lekcję\",\n rateStar: \"{n} z 5\",\n ratePlaceholder: \"Komentarz (opcjonalnie)\",\n rateSubmit: \"Zapisz ocenę\",\n rateSaved: \"Dziękujemy za ocenę.\",\n rateAverage: \"Średnia ocena {average} ({count})\",\n },\n en: {\n back: \"Back\",\n loading: \"Loading the course...\",\n retry: \"Try again\",\n errorGeneric: \"Could not load the data. Please try again in a moment.\",\n errorUnauthorized:\n \"You do not have access to this course. Sign in again or contact support.\",\n errorNotFound: \"This course or lesson was not found.\",\n errorUnavailable:\n \"The course is temporarily unavailable. Please try again in a moment.\",\n previewBadge: \"Preview\",\n progressLessons: \"{done} of {total} lessons\",\n progressPercent: \"{percent}% complete\",\n contents: \"Contents\",\n free: \"Free\",\n locked: \"Locked\",\n lockedByTest: \"Pass the test to unlock it: {test}\",\n lockedPage: \"This lesson is locked. Pass the test “{test}” to unlock it.\",\n testBadge: \"Test\",\n completed: \"Completed\",\n continueCourse: \"Continue the course\",\n noItem: \"Pick a lesson from the list.\",\n lessonsTab: \"Lessons\",\n descriptionTab: \"Description\",\n filesTab: \"Files\",\n questionsTab: \"Questions\",\n upNext: \"Next lesson: {title}\",\n openNext: \"Continue\",\n courseCompleted: \"Course completed\",\n lessonNavigation: \"Lesson navigation\",\n mobilePrev: \"Previous\",\n mobileNext: \"Next\",\n markComplete: \"Mark as completed\",\n markedComplete: \"Lesson completed\",\n prevItem: \"Previous\",\n nextItem: \"Next\",\n attachments: \"Downloads\",\n download: \"Download\",\n autoplay: \"Autoplay the next lesson\",\n videoError: \"The video could not be played.\",\n videoRetry: \"Reload the video\",\n noContent: \"This lesson has no content yet.\",\n testGating: \"Passing this test unlocks the following lessons.\",\n testAttemptsLeft: \"Attempts left: {count}\",\n testAttemptsUnlimited: \"The number of attempts is unlimited.\",\n testNoAttemptsLeft: \"You have used all attempts.\",\n testPassThreshold: \"Passing score: {percent}%\",\n testBestScore: \"Best score: {percent}%\",\n testSubmit: \"Check answers\",\n testSubmitting: \"Checking...\",\n testUnanswered: \"Unanswered questions: {count}\",\n testPassed: \"Test passed\",\n testFailed: \"Test not passed\",\n testScore: \"Score: {percent}%\",\n testCorrect: \"Correct\",\n testIncorrect: \"Incorrect\",\n testCorrectAnswer: \"Correct answer\",\n testTryAgain: \"Try again\",\n testPreviousAttempts: \"Previous attempts\",\n testAttemptRow: \"{percent}% · {date}\",\n testNoQuestions: \"This test has no questions yet.\",\n mcqSingle: \"Choose one answer\",\n mcqMultiple: \"Select all correct answers\",\n todoHint: \"Check the steps you have done\",\n todoRequired: \"required\",\n gapPlaceholder: \"fill in\",\n sortHint: \"Put the items in the correct order\",\n moveUp: \"Move up\",\n moveDown: \"Move down\",\n questionsTitle: \"Questions about this lesson\",\n questionsEmpty: \"Nobody has asked a question yet.\",\n questionPlaceholder: \"Ask the course author a question\",\n questionSubmit: \"Send question\",\n questionSent: \"Question sent.\",\n questionMine: \"You\",\n questionTeamAnswer: \"Answer from the author\",\n rateTitle: \"Rate this lesson\",\n rateStar: \"{n} of 5\",\n ratePlaceholder: \"Comment (optional)\",\n rateSubmit: \"Save rating\",\n rateSaved: \"Thanks for your rating.\",\n rateAverage: \"Average rating {average} ({count})\",\n },\n};\n\n/** Course strings for a locale (library locale by default) with overrides. */\nexport function getCourseTexts(\n overrides?: Partial<CourseTexts>,\n locale: string = getLocale(),\n): CourseTexts {\n return resolveTexts(COURSE_TEXT_TABLES, locale, overrides);\n}\n","import { ArrowDown, ArrowUp, Check, X } from \"lucide-react\";\nimport { useRef, useState } from \"react\";\nimport { cn } from \"../lib/utils\";\nimport { Button } from \"../ui/button\";\nimport { Input } from \"../ui/input\";\nimport { scoreBucket } from \"@/src/analytics/instrument\";\nimport { useCourseTrack } from \"./analytics\";\nimport { useCourseView } from \"./context\";\nimport {\n countUnanswered,\n describeAnswer,\n emptyAnswer,\n fillTemplate,\n moveItem,\n splitGapTemplate,\n} from \"./logic\";\nimport type {\n QuestionAnswer,\n Test,\n TestAnswers,\n TestQuestion,\n TestResult,\n TestSubmissionList,\n} from \"./types\";\nimport { ACCENT_BUTTON, CARD, ErrorBox, LoadingBlock, errorMessage } from \"./ui\";\nimport { useAsyncResource } from \"./useAsyncResource\";\n\ntype AnswerEditorProps<A extends QuestionAnswer> = {\n question: TestQuestion;\n answer: A;\n disabled: boolean;\n onChange: (answer: A) => void;\n};\n\nfunction McqEditor({\n question,\n answer,\n disabled,\n onChange,\n}: AnswerEditorProps<Extract<QuestionAnswer, { type: \"mcq\" }>>) {\n const { texts } = useCourseView();\n if (question.config.type !== \"mcq\") return null;\n const { choices, multiple } = question.config;\n return (\n <fieldset className=\"space-y-2\" disabled={disabled}>\n <legend className=\"mb-2 text-xs text-neutral-500 dark:text-neutral-400\">\n {multiple ? texts.mcqMultiple : texts.mcqSingle}\n </legend>\n {choices.map((choice) => {\n const checked = answer.selectedIds.includes(choice.id);\n return (\n <label\n key={choice.id}\n className=\"flex cursor-pointer items-start gap-3 rounded-lg bg-neutral-100 px-3 py-2 text-sm dark:bg-neutral-800\"\n >\n <input\n className=\"mt-0.5\"\n type={multiple ? \"checkbox\" : \"radio\"}\n name={`q-${question.id}`}\n checked={checked}\n onChange={() =>\n onChange({\n type: \"mcq\",\n selectedIds: multiple\n ? checked\n ? answer.selectedIds.filter((id) => id !== choice.id)\n : [...answer.selectedIds, choice.id]\n : [choice.id],\n })\n }\n />\n <span className=\"break-words\">{choice.label}</span>\n </label>\n );\n })}\n </fieldset>\n );\n}\n\nfunction TodolistEditor({\n question,\n answer,\n disabled,\n onChange,\n}: AnswerEditorProps<Extract<QuestionAnswer, { type: \"todolist\" }>>) {\n const { texts } = useCourseView();\n if (question.config.type !== \"todolist\") return null;\n return (\n <fieldset className=\"space-y-2\" disabled={disabled}>\n <legend className=\"mb-2 text-xs text-neutral-500 dark:text-neutral-400\">\n {texts.todoHint}\n </legend>\n {question.config.steps.map((step) => {\n const checked = answer.checkedIds.includes(step.id);\n return (\n <label\n key={step.id}\n className=\"flex cursor-pointer items-start gap-3 rounded-lg bg-neutral-100 px-3 py-2 text-sm dark:bg-neutral-800\"\n >\n <input\n className=\"mt-0.5\"\n type=\"checkbox\"\n checked={checked}\n onChange={() =>\n onChange({\n type: \"todolist\",\n checkedIds: checked\n ? answer.checkedIds.filter((id) => id !== step.id)\n : [...answer.checkedIds, step.id],\n })\n }\n />\n <span className=\"break-words\">\n {step.label}\n {step.required && (\n <span className=\"ml-2 text-xs text-neutral-500 dark:text-neutral-400\">\n ({texts.todoRequired})\n </span>\n )}\n </span>\n </label>\n );\n })}\n </fieldset>\n );\n}\n\nfunction FillGapEditor({\n question,\n answer,\n disabled,\n onChange,\n}: AnswerEditorProps<Extract<QuestionAnswer, { type: \"fill_gap\" }>>) {\n const { texts } = useCourseView();\n if (question.config.type !== \"fill_gap\") return null;\n const segments = splitGapTemplate(\n question.config.template,\n question.config.blanks,\n );\n return (\n <p className=\"text-sm leading-10 break-words\">\n {segments.map((segment, index) =>\n segment.type === \"text\" ? (\n <span key={index} className=\"whitespace-pre-wrap\">\n {segment.text}\n </span>\n ) : (\n <Input\n key={index}\n className=\"mx-1 inline-block h-8 w-36 align-middle\"\n aria-label={`${texts.gapPlaceholder} ${segment.id}`}\n placeholder={texts.gapPlaceholder}\n disabled={disabled}\n value={answer.values[segment.id] ?? \"\"}\n onChange={(e) =>\n onChange({\n type: \"fill_gap\",\n values: { ...answer.values, [segment.id]: e.target.value },\n })\n }\n />\n ),\n )}\n </p>\n );\n}\n\nfunction SortEditor({\n question,\n answer,\n disabled,\n onChange,\n}: AnswerEditorProps<Extract<QuestionAnswer, { type: \"sort_order\" }>>) {\n const { texts } = useCourseView();\n if (question.config.type !== \"sort_order\") return null;\n const labels = new Map(question.config.items.map((i) => [i.id, i.label]));\n const move = (from: number, to: number) =>\n onChange({\n type: \"sort_order\",\n orderedIds: moveItem(answer.orderedIds, from, to),\n });\n return (\n <div className=\"space-y-2\">\n <p className=\"text-xs text-neutral-500 dark:text-neutral-400\">\n {texts.sortHint}\n </p>\n <ol className=\"space-y-2\">\n {answer.orderedIds.map((id, index) => (\n <li\n key={id}\n className=\"flex items-center gap-2 rounded-lg bg-neutral-100 px-3 py-2 text-sm dark:bg-neutral-800\"\n >\n <span className=\"w-5 text-xs text-neutral-500\">{index + 1}.</span>\n <span className=\"min-w-0 flex-1 break-words\">{labels.get(id) ?? id}</span>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n aria-label={texts.moveUp}\n disabled={disabled || index === 0}\n onClick={() => move(index, index - 1)}\n >\n <ArrowUp aria-hidden />\n </Button>\n <Button\n type=\"button\"\n variant=\"ghost\"\n size=\"icon-xs\"\n aria-label={texts.moveDown}\n disabled={disabled || index === answer.orderedIds.length - 1}\n onClick={() => move(index, index + 1)}\n >\n <ArrowDown aria-hidden />\n </Button>\n </li>\n ))}\n </ol>\n </div>\n );\n}\n\nfunction QuestionEditor({\n question,\n answer,\n disabled,\n onChange,\n}: {\n question: TestQuestion;\n answer: QuestionAnswer;\n disabled: boolean;\n onChange: (answer: QuestionAnswer) => void;\n}) {\n const common = { question, disabled, onChange };\n switch (answer.type) {\n case \"mcq\":\n return <McqEditor {...common} answer={answer} />;\n case \"todolist\":\n return <TodolistEditor {...common} answer={answer} />;\n case \"fill_gap\":\n return <FillGapEditor {...common} answer={answer} />;\n case \"sort_order\":\n return <SortEditor {...common} answer={answer} />;\n }\n}\n\nfunction SubmissionHistory({ list }: { list: TestSubmissionList }) {\n const { texts } = useCourseView();\n if (list.submissions.length === 0) return null;\n return (\n <section className=\"space-y-2\">\n <h3 className=\"text-sm font-semibold\">{texts.testPreviousAttempts}</h3>\n <ul className=\"space-y-1 text-sm\">\n {list.submissions.map((submission) => (\n <li\n key={submission.id}\n className=\"flex items-center gap-2 text-neutral-700 dark:text-neutral-300\"\n >\n {submission.passed ? (\n <Check className=\"size-4 text-emerald-600\" aria-hidden />\n ) : (\n <X className=\"size-4 text-red-600\" aria-hidden />\n )}\n {fillTemplate(texts.testAttemptRow, {\n percent: submission.scorePercent,\n date: submission.createdAt?.toLocaleString() ?? \"\",\n })}\n </li>\n ))}\n </ul>\n </section>\n );\n}\n\ntype TestFormProps = {\n test: Test;\n onSubmitted: (result: TestResult) => void;\n onTryAgain: () => void;\n};\n\nfunction TestForm({ test, onSubmitted, onTryAgain }: TestFormProps) {\n const { dataSource, texts } = useCourseView();\n const [answers, setAnswers] = useState<TestAnswers>({});\n const [result, setResult] = useState<TestResult>();\n const [submitting, setSubmitting] = useState(false);\n const [error, setError] = useState<unknown>();\n const startedAt = useRef(Date.now());\n // The questions as they were shown: a refreshed test would shuffle again.\n const questions = useRef(test.questions).current;\n\n const outOfAttempts = test.attemptsLeft != null && test.attemptsLeft <= 0;\n const attemptsLeft = result ? result.attemptsLeft : test.attemptsLeft;\n const unanswered = countUnanswered(questions, answers);\n\n const answerOf = (q: TestQuestion): QuestionAnswer =>\n answers[q.id] ?? emptyAnswer(q.config);\n\n async function submit() {\n setSubmitting(true);\n setError(undefined);\n try {\n // The shuffled order of a sort question that was not touched is an answer too.\n const complete: TestAnswers = {};\n for (const question of questions) {\n complete[question.id] = answerOf(question);\n }\n const submitted = await dataSource.submitTest(\n test.id,\n complete,\n Date.now() - startedAt.current,\n );\n setResult(submitted);\n onSubmitted(submitted);\n } catch (e) {\n setError(e);\n } finally {\n setSubmitting(false);\n }\n }\n\n const resultByQuestion = new Map(result?.results.map((r) => [r.questionId, r]));\n\n if (questions.length === 0) {\n return (\n <p className=\"text-sm text-neutral-500 dark:text-neutral-400\">\n {texts.testNoQuestions}\n </p>\n );\n }\n\n return (\n <form\n className=\"space-y-6\"\n onSubmit={(e) => {\n e.preventDefault();\n if (result == null && !outOfAttempts) void submit();\n }}\n >\n {result && (\n <div\n role=\"status\"\n className={cn(\n \"rounded-xl p-4 text-sm\",\n result.submission.passed\n ? \"bg-emerald-50 text-emerald-800 dark:bg-emerald-950 dark:text-emerald-300\"\n : \"bg-red-50 text-red-800 dark:bg-red-950 dark:text-red-300\",\n )}\n >\n <p className=\"text-base font-semibold\">\n {result.submission.passed ? texts.testPassed : texts.testFailed}\n </p>\n <p>\n {fillTemplate(texts.testScore, {\n percent: result.submission.scorePercent,\n })}\n </p>\n </div>\n )}\n\n <ol className=\"space-y-6\">\n {questions.map((question, index) => {\n const questionResult = resultByQuestion.get(question.id);\n const correctText =\n questionResult?.correctAnswer != null\n ? describeAnswer(question, questionResult.correctAnswer)\n : \"\";\n return (\n <li key={question.id} className=\"space-y-3\" data-question-id={question.id}>\n <p className=\"font-medium break-words\">\n {index + 1}. {question.prompt}\n </p>\n <QuestionEditor\n question={question}\n answer={answerOf(question)}\n disabled={result != null || submitting || outOfAttempts}\n onChange={(answer) =>\n setAnswers((current) => ({ ...current, [question.id]: answer }))\n }\n />\n {questionResult && (\n <div\n className={cn(\n \"flex items-start gap-2 text-sm\",\n questionResult.correct\n ? \"text-emerald-700 dark:text-emerald-400\"\n : \"text-red-700 dark:text-red-400\",\n )}\n >\n {questionResult.correct ? (\n <Check className=\"mt-0.5 size-4 shrink-0\" aria-hidden />\n ) : (\n <X className=\"mt-0.5 size-4 shrink-0\" aria-hidden />\n )}\n <span>\n {questionResult.correct ? texts.testCorrect : texts.testIncorrect}\n {correctText !== \"\" && !questionResult.correct && (\n <span className=\"block text-neutral-700 dark:text-neutral-300\">\n {texts.testCorrectAnswer}: {correctText}\n </span>\n )}\n </span>\n </div>\n )}\n </li>\n );\n })}\n </ol>\n\n {error != null && (\n <p role=\"alert\" className=\"text-sm text-red-600 dark:text-red-400\">\n {errorMessage(error, texts)}\n </p>\n )}\n\n {result == null ? (\n <div className=\"space-y-2\">\n {unanswered > 0 && !outOfAttempts && (\n <p className=\"text-xs text-neutral-500 dark:text-neutral-400\">\n {fillTemplate(texts.testUnanswered, { count: unanswered })}\n </p>\n )}\n <Button\n type=\"submit\"\n className={ACCENT_BUTTON}\n disabled={submitting || outOfAttempts}\n >\n {submitting ? texts.testSubmitting : texts.testSubmit}\n </Button>\n </div>\n ) : (\n attemptsLeft !== 0 &&\n !result.submission.passed && (\n <Button type=\"button\" variant=\"outline\" onClick={onTryAgain}>\n {texts.testTryAgain}\n </Button>\n )\n )}\n </form>\n );\n}\n\n/** A test: intro, questions, submission and result. */\nexport function TestPane({\n testId,\n title,\n onSubmitted,\n}: {\n testId: string;\n /** Title from the course structure, shown while the test loads. The owner gives this component a `key` of the test id. */\n title: string;\n /** Called after a submission so the owner can reload the unlocked structure. */\n onSubmitted: (result: TestResult) => void;\n}) {\n const { dataSource, texts } = useCourseView();\n const track = useCourseTrack();\n const test = useAsyncResource<Test>(\n () => dataSource.getTest(testId),\n [dataSource, testId],\n );\n const history = useAsyncResource<TestSubmissionList | undefined>(\n () => dataSource.listSubmissions(testId).catch(() => undefined),\n [dataSource, testId],\n );\n // A new form (new shuffle, empty answers) after \"try again\".\n const [formKey, setFormKey] = useState(0);\n\n return (\n <article data-test-id={testId} className={cn(CARD, \"min-w-0 space-y-5\")}>\n <h2 className=\"text-2xl font-semibold break-words\">\n {test.status === \"success\" ? test.data.title : title}\n </h2>\n\n {test.status === \"loading\" && <LoadingBlock />}\n {test.status === \"error\" && (\n <ErrorBox error={test.error} onRetry={test.reload} />\n )}\n {test.status === \"success\" && (\n <>\n {test.data.description.trim() !== \"\" && (\n <p className=\"text-sm break-words whitespace-pre-wrap text-neutral-700 dark:text-neutral-300\">\n {test.data.description}\n </p>\n )}\n <ul className=\"space-y-1 text-sm text-neutral-600 dark:text-neutral-400\">\n {test.data.gating && <li>{texts.testGating}</li>}\n <li>\n {fillTemplate(texts.testPassThreshold, {\n percent: test.data.passThresholdPercent,\n })}\n </li>\n <li>\n {test.data.attemptsLeft == null\n ? texts.testAttemptsUnlimited\n : fillTemplate(texts.testAttemptsLeft, {\n count: test.data.attemptsLeft,\n })}\n </li>\n {test.data.attemptCount > 0 && (\n <li>\n {fillTemplate(texts.testBestScore, {\n percent: test.data.bestScorePercent,\n })}\n </li>\n )}\n </ul>\n {test.data.attemptsLeft != null && test.data.attemptsLeft <= 0 && (\n <p role=\"alert\" className=\"text-sm font-medium text-red-700 dark:text-red-400\">\n {texts.testNoAttemptsLeft}\n </p>\n )}\n <TestForm\n key={formKey}\n test={test.data}\n onSubmitted={(result) => {\n track(\"test_submitted\", {\n lesson_id: testId,\n score_bucket: scoreBucket(result.submission.scorePercent),\n });\n onSubmitted(result);\n history.refresh();\n test.refresh();\n }}\n onTryAgain={() => setFormKey((key) => key + 1)}\n />\n {history.status === \"success\" && history.data && (\n <SubmissionHistory list={history.data} />\n )}\n </>\n )}\n </article>\n );\n}\n"],"mappings":"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","names":["ChevronLeft","List","Play","useCallback","useEffect","useId","useMemo","useRef","useState","accentVars","colors","vars","brand","brandDark","foreground","readableForeground","useCallback","createContext","useContext","CourseViewContext","CourseViewProvider","useCourseView","value","useCourseTrack","analytics","preview","useCourseView","useCallback","event","props","CheckCircle2","ClipboardCheck","Lock","PlayCircle","FileText","useEffect","useId","useRef","useState","flattenItems","course","section","item","findItem","itemId","getAdjacentItems","items","index","getAutoplayTarget","next","computeProgress","total","completed","markLessonCompleted","lessonId","sections","progress","resolveInitialItemId","requestedId","fillTemplate","template","values","match","key","isPlaybackComplete","positionSeconds","durationSeconds","resumePosition","positionSeconds","durationSeconds","formatDuration","totalSeconds","seconds","h","m","s","formatBytes","bytes","units","exponent","value","emptyAnswer","config","item","isAnswered","question","answer","countUnanswered","questions","answers","q","moveItem","list","from","to","copy","splitGapTemplate","template","blanks","segments","pattern","last","match","id","describeAnswer","label","entry","segment","jsx","jsxs","ItemIcon","item","Lock","CheckCircle2","ClipboardCheck","PlayCircle","FileText","OutlineItem","selected","variant","onSelect","texts","useCourseView","lockText","fillTemplate","state","cn","formatDuration","CourseOutline","course","selectedId","className","outlineId","useId","rootRef","useRef","currentSection","section","openSections","setOpenSections","useState","useEffect","current","renderItems","items","lessons","i","done","next","createClient","enumDesc","fileDesc","messageDesc","serviceDesc","file_google_protobuf_timestamp","file_public_v1_user_panel_course","UserPanelCourseService","serviceDesc","file_public_v1_user_panel_course","Code","ConnectError","timestampDate","CourseError","code","message","FAILURE_CODES","failureToCode","failure","toCourseError","error","CourseError","connectError","ConnectError","Code","unwrap","response","result","mapItem","item","mapCourse","course","section","mapRef","ref","mapLesson","lesson","a","timestampDate","mapQuestion","question","base","config","c","s","mapTest","test","q","mapSubmission","submission","mapAnswerFromProto","answer","value","mapAnswerToProto","questionId","mapLessonQuestion","createCourseDataSource","client","auth","credentials","call","run","lessonId","input","saved","testId","answers","durationMs","r","list","content","score","comment","rating","createRpcCourseDataSource","options","host","getTransportHost","client","createClient","UserPanelCourseService","getRpcTransport","createCourseDataSource","projectID","panelHost","getPanelConfig","getPanelSession","ChevronLeft","ChevronRight","Download","Paperclip","useEffect","useId","useMemo","useRef","useState","useState","Star","useState","jsx","RatingStars","value","onSelect","disabled","texts","useCourseView","hover","setHover","useState","shown","fillTemplate","n","filled","star","Star","cn","AlertTriangle","jsx","jsxs","ACCENT_BG","ACCENT_BUTTON","cn","CARD","errorMessage","error","texts","CourseError","ErrorBox","onRetry","className","useCourseView","AlertTriangle","Button","LoadingBlock","label","Skeleton","ProgressBar","percent","value","useCallback","useEffect","useRef","useState","useAsyncResource","load","deps","initialData","state","setState","reloadKey","setReloadKey","requestId","loadRef","skipFirst","id","data","error","reload","key","refresh","mutate","update","current","jsx","jsxs","LessonRatingBox","lessonId","initial","dataSource","texts","preview","useCourseView","track","useCourseTrack","rating","setRating","useState","score","setScore","comment","setComment","state","setState","error","setError","save","e","RatingStars","n","fillTemplate","Textarea","Button","ACCENT_BUTTON","errorMessage","formatDate","date","LessonQuestions","questions","useAsyncResource","content","setContent","ask","text","created","list","ErrorBox","question","DOMPurify","SANITIZE_CONFIG","createHtmlSanitizer","purify","sanitize","node","html","sharedSanitizer","sanitizeLessonHtml","Hls","useCallback","useEffect","useRef","useState","jsx","jsxs","canPlayNativeHls","video","VideoPlayer","src","poster","startAt","durationHint","autoPlay","lessonId","onReport","onEnded","onExpired","className","texts","useCourseView","track","useCourseTrack","trackRef","useRef","videoRef","failed","setFailed","useState","attempt","setAttempt","callbacks","lastReported","report","useCallback","reason","positionSeconds","duration","useEffect","hls","retriedNetwork","retriedMedia","timer","start","Hls","_event","data","status","onPlay","onPause","milestones","lastTime","watchedTo","sendMilestones","percent","milestone","newMilestones","onTimeUpdate","now","onVideoEnded","onVideoError","cn","Button","n","jsx","jsxs","LessonPane","lesson","mobileOutline","nextItem","position","courseType","prev","next","autoplay","onAutoplayChange","startPlaying","onNavigate","onProgressSaved","onEnded","onUrlsExpired","dataSource","texts","preview","useCourseView","track","useCourseTrack","completed","setCompleted","useState","completedRef","useRef","lastSavedPosition","lastKnownPosition","saving","setSaving","ended","setEnded","announced","setAnnounced","tabId","useId","activeTab","setActiveTab","useEffect","expiresAt","delay","timer","html","useMemo","sanitizeLessonHtml","save","positionSeconds","durationSeconds","markCompleted","saved","handleReport","report","reachedEnd","isPlaybackComplete","error","markComplete","description","hasBody","tabs","selectedTab","tab","panelClass","id","cn","panelProps","nextLocked","completionButton","Button","ACCENT_BUTTON","navigation","mobile","ChevronLeft","ChevronRight","VideoPlayer","resumePosition","index","event","offset","target","CARD","e","Paperclip","file","formatBytes","Download","LessonRatingBox","LessonQuestions","fillTemplate","COURSE_TEXT_TABLES","getCourseTexts","overrides","locale","getLocale","resolveTexts","ArrowDown","ArrowUp","Check","X","useRef","useState","Fragment","jsx","jsxs","McqEditor","question","answer","disabled","onChange","texts","useCourseView","choices","multiple","choice","checked","id","TodolistEditor","step","FillGapEditor","segments","splitGapTemplate","segment","index","Input","e","SortEditor","labels","move","from","to","moveItem","Button","ArrowUp","ArrowDown","QuestionEditor","common","SubmissionHistory","list","submission","Check","X","fillTemplate","TestForm","test","onSubmitted","onTryAgain","dataSource","answers","setAnswers","useState","result","setResult","submitting","setSubmitting","error","setError","startedAt","useRef","questions","outOfAttempts","attemptsLeft","unanswered","countUnanswered","answerOf","q","emptyAnswer","submit","complete","submitted","resultByQuestion","r","cn","questionResult","correctText","describeAnswer","current","errorMessage","ACCENT_BUTTON","TestPane","testId","title","track","useCourseTrack","useAsyncResource","history","formKey","setFormKey","CARD","LoadingBlock","ErrorBox","scoreBucket","key","jsx","jsxs","AUTOPLAY_KEY","useAutoplayPreference","value","setValue","useState","useEffect","update","useCallback","next","toRef","item","LockedPane","texts","useCourseView","cn","CARD","fillTemplate","LessonContainer","course","autoplay","onAutoplayChange","startPlaying","onNavigate","onProgressSaved","onEnded","onLocked","mobileOutline","dataSource","lesson","useAsyncResource","lockedByServer","CourseError","LoadingBlock","errorMessage","ErrorBox","adjacent","getAdjacentItems","position","flattenItems","entry","LessonPane","findItem","CourseView","selectedId","onSelect","onBack","onCourseChange","onCourseRefresh","preview","track","useCourseTrack","rootRef","useRef","contentsId","useId","contentsOpen","setContentsOpen","setAutoplay","playOnOpen","setPlayOnOpen","currentId","resolveInitialItemId","open","itemId","play","outline","CourseOutline","content","TestPane","ref","lessonId","saved","current","markLessonCompleted","target","getAutoplayTarget","isVideo","showMobileContents","showResume","Button","ChevronLeft","Play","ProgressBar","List","PanelCourse","accessId","accessToken","customDataSource","onItemChange","initialCourse","textOverrides","locale","theme","className","analytics","panel","usePanelConfig","instrument","useInstrument","useMemo","getCourseTexts","createRpcCourseDataSource","internalItemId","setInternalItemId","select","style","accentVars","courseId","viewValue","InstrumentProvider","CourseViewProvider"]}