@burdenoff/microfe-buildmyiq 2026.1028.2 → 2026.1029.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"TutorReviewMarker.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/components/TutorReviewMarker.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAGhC,OAAO,KAAK,EAAE,oBAAoB,EAAwB,MAAM,UAAU,CAAC;AAc3E,UAAU,sBAAsB;IAC9B,MAAM,EAAE,oBAAoB,CAAC;IAC7B,qEAAqE;IACrE,QAAQ,CAAC,EAAE,SAAS,GAAG,UAAU,CAAC;CACnC;AAED,eAAO,MAAM,iBAAiB,EAAE,EAAE,CAAC,sBAAsB,CA2CxD,CAAC"}
1
+ {"version":3,"file":"TutorReviewMarker.d.ts","sourceRoot":"","sources":["../../../src/buildmyiq/components/TutorReviewMarker.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AAGhC,OAAO,KAAK,EAAE,oBAAoB,EAAwB,MAAM,UAAU,CAAC;AAgB3E,UAAU,sBAAsB;IAC9B,MAAM,EAAE,oBAAoB,CAAC;IAC7B,qEAAqE;IACrE,QAAQ,CAAC,EAAE,SAAS,GAAG,UAAU,CAAC;CACnC;AAED,eAAO,MAAM,iBAAiB,EAAE,EAAE,CAAC,sBAAsB,CA2CxD,CAAC"}
@@ -8,8 +8,8 @@ var o = {
8
8
  REDACTED: t
9
9
  }, s = {
10
10
  ANNOTATED: "border-border-seam bg-bg-sunken text-text-secondary",
11
- CORRECTED: "border-status-success-text bg-status-success-bg text-status-success-text",
12
- REDACTED: "border-status-warning-border bg-status-warning-bg text-status-warning-text"
11
+ CORRECTED: "border-status-success-border bg-status-success-bg-subtle text-status-success-text",
12
+ REDACTED: "border-status-warning-border bg-status-warning-bg-subtle text-status-warning-text"
13
13
  }, c = ({ review: t, audience: n = "learner" }) => {
14
14
  let r = e(), c = o[t.status], l = n === "learner" ? {
15
15
  ANNOTATED: r("buildmyiq.tutor.review.annotatedLearner", "Reviewed by your instructor"),
@@ -1 +1 @@
1
- {"version":3,"file":"TutorReviewMarker.js","names":[],"sources":["../../../src/buildmyiq/components/TutorReviewMarker.tsx"],"sourcesContent":["/**\n * TutorReviewMarker — shows that an instructor reviewed an AI tutor answer\n * (BOFF-9663).\n * @module buildmyiq/components/TutorReviewMarker\n *\n * Rendered under an assistant answer whose `review` is set. The answer text the\n * server returns is already the reviewed version (the instructor's correction,\n * or a removal notice for a redacted answer); this marker tells the learner\n * that it was reviewed, how, and shows the instructor's note.\n */\n\nimport type { FC } from 'react';\nimport { EyeOff, MessageSquareText, PencilLine } from 'lucide-react';\nimport { useTr } from '../shared/hooks/useTr';\nimport type { BuiTutorAnswerReview, BuiTutorReviewStatus } from '../types';\n\nconst ICON: Record<BuiTutorReviewStatus, typeof PencilLine> = {\n ANNOTATED: MessageSquareText,\n CORRECTED: PencilLine,\n REDACTED: EyeOff,\n};\n\nconst TONE: Record<BuiTutorReviewStatus, string> = {\n ANNOTATED: 'border-border-seam bg-bg-sunken text-text-secondary',\n CORRECTED: 'border-status-success-text bg-status-success-bg text-status-success-text',\n REDACTED: 'border-status-warning-border bg-status-warning-bg text-status-warning-text',\n};\n\ninterface TutorReviewMarkerProps {\n review: BuiTutorAnswerReview;\n /** Wording for the audience: the learner reads \"your instructor\". */\n audience?: 'learner' | 'reviewer';\n}\n\nexport const TutorReviewMarker: FC<TutorReviewMarkerProps> = ({ review, audience = 'learner' }) => {\n const tr = useTr();\n const Icon = ICON[review.status];\n\n const headline: Record<BuiTutorReviewStatus, string> =\n audience === 'learner'\n ? {\n ANNOTATED: tr('buildmyiq.tutor.review.annotatedLearner', 'Reviewed by your instructor'),\n CORRECTED: tr(\n 'buildmyiq.tutor.review.correctedLearner',\n 'Reviewed by your instructor — answer corrected'\n ),\n REDACTED: tr(\n 'buildmyiq.tutor.review.redactedLearner',\n 'Reviewed by your instructor — answer removed'\n ),\n }\n : {\n ANNOTATED: tr('buildmyiq.tutor.review.annotated', 'Annotated'),\n CORRECTED: tr('buildmyiq.tutor.review.corrected', 'Corrected'),\n REDACTED: tr('buildmyiq.tutor.review.redacted', 'Redacted'),\n };\n\n return (\n <div\n data-testid=\"tutor-review-marker\"\n data-review-status={review.status}\n className={`mt-2 rounded-md border px-2.5 py-1.5 text-xs ${TONE[review.status]}`}\n >\n <p className=\"flex items-center gap-1.5 font-medium\">\n <Icon className=\"h-3.5 w-3.5 shrink-0\" />\n {headline[review.status]}\n </p>\n {review.note && (\n <p className=\"mt-1 whitespace-pre-wrap break-words text-text-secondary\">\n <span className=\"font-medium\">\n {tr('buildmyiq.tutor.review.noteLabel', 'Instructor note:')}\n </span>{' '}\n {review.note}\n </p>\n )}\n </div>\n );\n};\n"],"mappings":";;;;AAgBA,IAAM,IAAwD;CAC5D,WAAW;CACX,WAAW;CACX,UAAU;AACZ,GAEM,IAA6C;CACjD,WAAW;CACX,WAAW;CACX,UAAU;AACZ,GAQa,KAAiD,EAAE,WAAQ,cAAW,gBAAgB;CACjG,IAAM,IAAK,EAAM,GACX,IAAO,EAAK,EAAO,SAEnB,IACJ,MAAa,YACT;EACE,WAAW,EAAG,2CAA2C,6BAA6B;EACtF,WAAW,EACT,2CACA,gDACF;EACA,UAAU,EACR,0CACA,8CACF;CACF,IACA;EACE,WAAW,EAAG,oCAAoC,WAAW;EAC7D,WAAW,EAAG,oCAAoC,WAAW;EAC7D,UAAU,EAAG,mCAAmC,UAAU;CAC5D;CAEN,OACE,kBAAC,OAAD;EACE,eAAY;EACZ,sBAAoB,EAAO;EAC3B,WAAW,gDAAgD,EAAK,EAAO;YAHzE,CAKE,kBAAC,KAAD;GAAG,WAAU;aAAb,CACE,kBAAC,GAAD,EAAM,WAAU,uBAAwB,CAAA,GACvC,EAAS,EAAO,OAChB;MACF,EAAO,QACN,kBAAC,KAAD;GAAG,WAAU;aAAb;IACE,kBAAC,QAAD;KAAM,WAAU;eACb,EAAG,oCAAoC,kBAAkB;IACtD,CAAA;IAAE;IACP,EAAO;GACP;IAEF;;AAET"}
1
+ {"version":3,"file":"TutorReviewMarker.js","names":[],"sources":["../../../src/buildmyiq/components/TutorReviewMarker.tsx"],"sourcesContent":["/**\n * TutorReviewMarker — shows that an instructor reviewed an AI tutor answer\n * (BOFF-9663).\n * @module buildmyiq/components/TutorReviewMarker\n *\n * Rendered under an assistant answer whose `review` is set. The answer text the\n * server returns is already the reviewed version (the instructor's correction,\n * or a removal notice for a redacted answer); this marker tells the learner\n * that it was reviewed, how, and shows the instructor's note.\n */\n\nimport type { FC } from 'react';\nimport { EyeOff, MessageSquareText, PencilLine } from 'lucide-react';\nimport { useTr } from '../shared/hooks/useTr';\nimport type { BuiTutorAnswerReview, BuiTutorReviewStatus } from '../types';\n\nconst ICON: Record<BuiTutorReviewStatus, typeof PencilLine> = {\n ANNOTATED: MessageSquareText,\n CORRECTED: PencilLine,\n REDACTED: EyeOff,\n};\n\n// Tinted (`*-bg-subtle`) surfaces, not the solid `*-bg` fills: the solid fills\n// are saturated in dark mode, and the status text colour on them is unreadable.\nconst TONE: Record<BuiTutorReviewStatus, string> = {\n ANNOTATED: 'border-border-seam bg-bg-sunken text-text-secondary',\n CORRECTED: 'border-status-success-border bg-status-success-bg-subtle text-status-success-text',\n REDACTED: 'border-status-warning-border bg-status-warning-bg-subtle text-status-warning-text',\n};\n\ninterface TutorReviewMarkerProps {\n review: BuiTutorAnswerReview;\n /** Wording for the audience: the learner reads \"your instructor\". */\n audience?: 'learner' | 'reviewer';\n}\n\nexport const TutorReviewMarker: FC<TutorReviewMarkerProps> = ({ review, audience = 'learner' }) => {\n const tr = useTr();\n const Icon = ICON[review.status];\n\n const headline: Record<BuiTutorReviewStatus, string> =\n audience === 'learner'\n ? {\n ANNOTATED: tr('buildmyiq.tutor.review.annotatedLearner', 'Reviewed by your instructor'),\n CORRECTED: tr(\n 'buildmyiq.tutor.review.correctedLearner',\n 'Reviewed by your instructor — answer corrected'\n ),\n REDACTED: tr(\n 'buildmyiq.tutor.review.redactedLearner',\n 'Reviewed by your instructor — answer removed'\n ),\n }\n : {\n ANNOTATED: tr('buildmyiq.tutor.review.annotated', 'Annotated'),\n CORRECTED: tr('buildmyiq.tutor.review.corrected', 'Corrected'),\n REDACTED: tr('buildmyiq.tutor.review.redacted', 'Redacted'),\n };\n\n return (\n <div\n data-testid=\"tutor-review-marker\"\n data-review-status={review.status}\n className={`mt-2 rounded-md border px-2.5 py-1.5 text-xs ${TONE[review.status]}`}\n >\n <p className=\"flex items-center gap-1.5 font-medium\">\n <Icon className=\"h-3.5 w-3.5 shrink-0\" />\n {headline[review.status]}\n </p>\n {review.note && (\n <p className=\"mt-1 whitespace-pre-wrap break-words text-text-secondary\">\n <span className=\"font-medium\">\n {tr('buildmyiq.tutor.review.noteLabel', 'Instructor note:')}\n </span>{' '}\n {review.note}\n </p>\n )}\n </div>\n );\n};\n"],"mappings":";;;;AAgBA,IAAM,IAAwD;CAC5D,WAAW;CACX,WAAW;CACX,UAAU;AACZ,GAIM,IAA6C;CACjD,WAAW;CACX,WAAW;CACX,UAAU;AACZ,GAQa,KAAiD,EAAE,WAAQ,cAAW,gBAAgB;CACjG,IAAM,IAAK,EAAM,GACX,IAAO,EAAK,EAAO,SAEnB,IACJ,MAAa,YACT;EACE,WAAW,EAAG,2CAA2C,6BAA6B;EACtF,WAAW,EACT,2CACA,gDACF;EACA,UAAU,EACR,0CACA,8CACF;CACF,IACA;EACE,WAAW,EAAG,oCAAoC,WAAW;EAC7D,WAAW,EAAG,oCAAoC,WAAW;EAC7D,UAAU,EAAG,mCAAmC,UAAU;CAC5D;CAEN,OACE,kBAAC,OAAD;EACE,eAAY;EACZ,sBAAoB,EAAO;EAC3B,WAAW,gDAAgD,EAAK,EAAO;YAHzE,CAKE,kBAAC,KAAD;GAAG,WAAU;aAAb,CACE,kBAAC,GAAD,EAAM,WAAU,uBAAwB,CAAA,GACvC,EAAS,EAAO,OAChB;MACF,EAAO,QACN,kBAAC,KAAD;GAAG,WAAU;aAAb;IACE,kBAAC,QAAD;KAAM,WAAU;eACb,EAAG,oCAAoC,kBAAkB;IACtD,CAAA;IAAE;IACP,EAAO;GACP;IAEF;;AAET"}
@@ -1 +1 @@
1
- {"version":3,"file":"TutorReviewPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/tutorReview/TutorReviewPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAqB,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AA+WnD,eAAO,MAAM,eAAe,EAAE,EA8M7B,CAAC"}
1
+ {"version":3,"file":"TutorReviewPage.d.ts","sourceRoot":"","sources":["../../../../src/buildmyiq/pages/tutorReview/TutorReviewPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAqB,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AA+WnD,eAAO,MAAM,eAAe,EAAE,EAqN7B,CAAC"}
@@ -418,9 +418,9 @@ var V = 20, H = "__all__", U = ({ courseId: e, lessonId: t }) => {
418
418
  })]
419
419
  }),
420
420
  /* @__PURE__ */ j(P, {
421
- open: G != null,
421
+ open: G != null && D == null,
422
422
  onOpenChange: (e) => {
423
- e || E(null);
423
+ !e && D == null && E(null);
424
424
  },
425
425
  children: /* @__PURE__ */ j(F, {
426
426
  side: h ? "right" : "bottom",
@@ -1 +1 @@
1
- {"version":3,"file":"TutorReviewPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/tutorReview/TutorReviewPage.tsx"],"sourcesContent":["/**\n * TutorReviewPage — educator review of AI tutor conversations (BOFF-9663).\n * @module buildmyiq/pages/tutorReview\n *\n * Lists the workspace's tutor conversations (`buiListTutorSessions`, EDITOR+),\n * optionally filtered to one course. Opening a conversation shows every turn;\n * for each tutor answer a reviewer can annotate it, correct it, redact it, or\n * clear an earlier review (`buiReviewTutorAnswer`). The original answer is\n * never overwritten: it stays visible here with the full review history, while\n * the learner sees the reviewed version with a \"reviewed by your instructor\"\n * marker.\n */\n\nimport { useMemo, useState, type FC } from 'react';\nimport { Bot, EyeOff, History, MessageSquareText, PencilLine, Undo2 } from 'lucide-react';\nimport {\n Button,\n Sheet,\n SheetContent,\n SheetDescription,\n SheetHeader,\n SheetTitle,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport { useMediaQuery } from '@burdenoff/fe-libs/shared/hooks';\n\nimport {\n Avatar,\n DataTable,\n EmptyState,\n ErrorView,\n LoadingView,\n PageLayout,\n Pagination,\n TutorReviewMarker,\n type Column,\n} from '../../components';\nimport { BuiPermissionButton } from '../../components/BuiPermissionButton';\nimport { useBuildmyiq } from '../../context';\nimport {\n useBuiCourse,\n useBuiCourses,\n useBuiReviewTutorAnswer,\n useBuiTutorReviewSessions,\n useCursorPager,\n} from '../../hooks';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { formatDateTime } from '../../utils/formatters';\nimport type { BuiTutorMessage, BuiTutorReviewAction, BuiTutorSession } from '../../types';\nimport { Field, Modal, SelectField } from '../authoring/_forms';\nimport {\n MAX_CORRECTED_CONTENT_LENGTH,\n MAX_REVIEW_NOTE_LENGTH,\n buildReviewInput,\n summarizeSession,\n} from './tutorReviewModel';\n\nconst PAGE_SIZE = 20;\nconst ALL_COURSES = '__all__';\n\nconst LessonTitle: FC<{ courseId: string; lessonId: string }> = ({ courseId, lessonId }) => {\n const { data } = useBuiCourse(courseId);\n const title = data?.lessons.find((l) => l.id === lessonId)?.title;\n return (\n <span className=\"text-text-secondary\" title={lessonId}>\n {title ?? `${lessonId.slice(0, 8)}…`}\n </span>\n );\n};\n\ninterface ReviewTarget {\n message: BuiTutorMessage;\n action: BuiTutorReviewAction;\n}\n\nconst ReviewDialog: FC<{ target: ReviewTarget; onClose: () => void }> = ({ target, onClose }) => {\n const tr = useTr();\n const review = useBuiReviewTutorAnswer();\n const { message, action } = target;\n const [note, setNote] = useState(action === 'CLEAR' ? '' : (message.review?.note ?? ''));\n const [corrected, setCorrected] = useState(\n message.review?.status === 'CORRECTED'\n ? message.content\n : (message.originalContent ?? message.content)\n );\n const built = buildReviewInput(message.id, { action, note, correctedContent: corrected });\n\n const copy: Record<BuiTutorReviewAction, { title: string; description: string; cta: string }> = {\n ANNOTATE: {\n title: tr('buildmyiq.tutorReview.annotateTitle', 'Annotate answer'),\n description: tr(\n 'buildmyiq.tutorReview.annotateDescription',\n 'The answer stays as it is. The learner sees your note under it.'\n ),\n cta: tr('buildmyiq.tutorReview.annotate', 'Annotate'),\n },\n CORRECT: {\n title: tr('buildmyiq.tutorReview.correctTitle', 'Correct answer'),\n description: tr(\n 'buildmyiq.tutorReview.correctDescription',\n 'The learner sees your corrected answer instead of the original, which is kept for audit.'\n ),\n cta: tr('buildmyiq.tutorReview.correct', 'Correct'),\n },\n REDACT: {\n title: tr('buildmyiq.tutorReview.redactTitle', 'Redact answer'),\n description: tr(\n 'buildmyiq.tutorReview.redactDescription',\n 'The learner no longer sees this answer, and the tutor will not reuse it. The original is kept for audit.'\n ),\n cta: tr('buildmyiq.tutorReview.redact', 'Redact'),\n },\n CLEAR: {\n title: tr('buildmyiq.tutorReview.clearTitle', 'Clear review'),\n description: tr(\n 'buildmyiq.tutorReview.clearDescription',\n 'The learner sees the original answer again. The review stays in the history.'\n ),\n cta: tr('buildmyiq.tutorReview.clear', 'Clear review'),\n },\n };\n\n const handleSave = async (): Promise<void> => {\n if (!built.ok) return;\n try {\n await review.mutateAsync(built.input);\n onClose();\n } catch {\n // Shown inline from `review.error`.\n }\n };\n\n return (\n <Modal\n open\n onClose={onClose}\n title={copy[action].title}\n description={copy[action].description}\n footer={\n <>\n <Button variant=\"outline\" onClick={onClose}>\n {tr('common.cancel', 'Cancel')}\n </Button>\n <Button\n variant={action === 'REDACT' ? 'destructive' : 'default'}\n onClick={() => void handleSave()}\n disabled={review.isPending || !built.ok}\n data-testid=\"tutor-review-save\"\n >\n {copy[action].cta}\n </Button>\n </>\n }\n >\n <div className=\"grid gap-3 py-2\">\n <div className=\"rounded-md border border-border-seam bg-bg-sunken p-3\">\n <p className=\"mb-1 text-xs font-medium text-text-tertiary\">\n {tr('buildmyiq.tutorReview.originalAnswer', 'Original answer')}\n </p>\n <p className=\"max-h-40 overflow-y-auto whitespace-pre-wrap break-words text-sm text-text-secondary\">\n {message.originalContent ?? message.content}\n </p>\n </div>\n {action === 'CORRECT' && (\n <Field\n label={tr('buildmyiq.tutorReview.correctedLabel', 'Corrected answer')}\n htmlFor=\"tutor-review-corrected\"\n required\n hint={tr('buildmyiq.tutorReview.maxChars', 'Up to {{max}} characters.', {\n max: MAX_CORRECTED_CONTENT_LENGTH,\n })}\n >\n <Textarea\n id=\"tutor-review-corrected\"\n rows={6}\n value={corrected}\n onChange={(e) => setCorrected(e.target.value)}\n />\n </Field>\n )}\n {action !== 'CLEAR' && (\n <Field\n label={tr('buildmyiq.tutorReview.noteLabel', 'Note for the learner')}\n htmlFor=\"tutor-review-note\"\n required={action === 'ANNOTATE'}\n hint={tr('buildmyiq.tutorReview.maxChars', 'Up to {{max}} characters.', {\n max: MAX_REVIEW_NOTE_LENGTH,\n })}\n >\n <Textarea\n id=\"tutor-review-note\"\n rows={3}\n value={note}\n onChange={(e) => setNote(e.target.value)}\n />\n </Field>\n )}\n {!built.ok && (note.trim() || action === 'CORRECT') && (\n <p className=\"text-xs text-text-tertiary\">{built.reason}</p>\n )}\n {review.isError && (\n <p className=\"text-sm text-status-error-text\">\n {review.error instanceof Error\n ? review.error.message\n : tr('buildmyiq.tutorReview.saveError', 'Could not save the review.')}\n </p>\n )}\n </div>\n </Modal>\n );\n};\n\nconst ACTION_LABEL: Record<BuiTutorReviewAction, string> = {\n ANNOTATE: 'Annotated',\n CORRECT: 'Corrected',\n REDACT: 'Redacted',\n CLEAR: 'Review cleared',\n};\n\nconst AnswerCard: FC<{\n message: BuiTutorMessage;\n onAction: (target: ReviewTarget) => void;\n}> = ({ message, onAction }) => {\n const tr = useTr();\n const [showHistory, setShowHistory] = useState(false);\n const status = message.review?.status;\n const replaced = status === 'CORRECTED' || status === 'REDACTED';\n const history = message.reviewHistory ?? [];\n\n return (\n <div\n className=\"min-w-0 flex-1\"\n data-testid=\"tutor-review-answer\"\n data-message-id={message.id}\n data-review-status={status ?? 'NONE'}\n >\n <p className=\"text-sm font-semibold text-text-primary\">\n {tr('buildmyiq.tutorReview.tutor', 'AI Tutor')}\n </p>\n {replaced && (\n <p className=\"mt-1 text-xs font-medium text-text-tertiary\">\n {tr('buildmyiq.tutorReview.learnerSees', 'Learner sees')}\n </p>\n )}\n <p\n className={\n status === 'REDACTED'\n ? 'mt-0.5 whitespace-pre-wrap break-words text-sm italic text-text-tertiary'\n : 'mt-0.5 whitespace-pre-wrap break-words text-sm text-text-secondary'\n }\n >\n {message.content}\n </p>\n {message.review && <TutorReviewMarker review={message.review} audience=\"reviewer\" />}\n {replaced && message.originalContent != null && (\n <details className=\"mt-2 rounded-md border border-border-seam px-2.5 py-1.5\">\n <summary className=\"cursor-pointer text-xs font-medium text-text-secondary\">\n {tr('buildmyiq.tutorReview.originalAnswer', 'Original answer')}\n </summary>\n <p className=\"mt-1 whitespace-pre-wrap break-words text-sm text-text-secondary\">\n {message.originalContent}\n </p>\n </details>\n )}\n\n <BuiPermissionButton resource=\"buildmyiq-tutor\" action=\"update\" deniedBehavior=\"hide\">\n <div className=\"mt-2 flex flex-wrap gap-1.5\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => onAction({ message, action: 'ANNOTATE' })}\n >\n <MessageSquareText className=\"mr-1 h-4 w-4\" />\n {tr('buildmyiq.tutorReview.annotate', 'Annotate')}\n </Button>\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => onAction({ message, action: 'CORRECT' })}\n >\n <PencilLine className=\"mr-1 h-4 w-4\" />\n {tr('buildmyiq.tutorReview.correct', 'Correct')}\n </Button>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"text-status-error-text hover:text-status-error-text\"\n onClick={() => onAction({ message, action: 'REDACT' })}\n >\n <EyeOff className=\"mr-1 h-4 w-4\" />\n {tr('buildmyiq.tutorReview.redact', 'Redact')}\n </Button>\n {status && (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => onAction({ message, action: 'CLEAR' })}\n >\n <Undo2 className=\"mr-1 h-4 w-4\" />\n {tr('buildmyiq.tutorReview.clear', 'Clear review')}\n </Button>\n )}\n </div>\n </BuiPermissionButton>\n\n {history.length > 0 && (\n <div className=\"mt-2\">\n <button\n type=\"button\"\n className=\"inline-flex items-center gap-1 text-xs text-text-secondary hover:text-text-primary\"\n onClick={() => setShowHistory((v) => !v)}\n aria-expanded={showHistory}\n >\n <History className=\"h-3.5 w-3.5\" />\n {tr('buildmyiq.tutorReview.history', 'Review history ({{count}})', {\n count: history.length,\n })}\n </button>\n {showHistory && (\n <ol className=\"mt-1.5 grid gap-1.5 border-l border-border-seam pl-3\">\n {history.map((entry) => (\n <li key={entry.id} className=\"text-xs text-text-secondary\">\n <span className=\"font-medium text-text-primary\">\n {tr(`buildmyiq.tutorReview.action.${entry.action}`, ACTION_LABEL[entry.action])}\n </span>{' '}\n · {formatDateTime(entry.createdAt)} ·{' '}\n <span className=\"font-mono\" title={entry.reviewerId}>\n {entry.reviewerId.slice(0, 8)}…\n </span>\n {entry.note && <p className=\"mt-0.5 whitespace-pre-wrap\">{entry.note}</p>}\n </li>\n ))}\n </ol>\n )}\n </div>\n )}\n </div>\n );\n};\n\nconst Transcript: FC<{ session: BuiTutorSession; onAction: (target: ReviewTarget) => void }> = ({\n session,\n onAction,\n}) => {\n const tr = useTr();\n if (session.messages.length === 0) {\n return (\n <p className=\"text-sm text-text-secondary\">\n {tr('buildmyiq.tutorReview.noMessages', 'This conversation has no messages yet.')}\n </p>\n );\n }\n return (\n <ul className=\"flex flex-col gap-5\">\n {session.messages.map((message) =>\n message.role === 'ASSISTANT' ? (\n <li key={message.id} className=\"flex gap-3\">\n <span className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[var(--color-accent-soft)]\">\n <Bot className=\"h-4 w-4 text-[var(--color-accent-teal)]\" />\n </span>\n <AnswerCard message={message} onAction={onAction} />\n </li>\n ) : (\n <li key={message.id} className=\"flex gap-3\">\n <Avatar size=\"sm\" seed={session.learnerId} name={null} />\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-semibold text-text-primary\">\n {tr('buildmyiq.tutorReview.learner', 'Learner')}\n </p>\n <p className=\"mt-0.5 whitespace-pre-wrap break-words text-sm text-text-secondary\">\n {message.content}\n </p>\n </div>\n </li>\n )\n )}\n </ul>\n );\n};\n\nexport const TutorReviewPage: FC = () => {\n const tr = useTr();\n const { currentUser } = useBuildmyiq();\n const isDesktop = useMediaQuery('(min-width: 768px)');\n const pager = useCursorPager();\n const [courseId, setCourseId] = useState<string>(ALL_COURSES);\n const [activeId, setActiveId] = useState<string | null>(null);\n const [target, setTarget] = useState<ReviewTarget | null>(null);\n\n const coursesQuery = useBuiCourses({ limit: 100 });\n const sessionsQuery = useBuiTutorReviewSessions({\n courseId: courseId === ALL_COURSES ? undefined : courseId,\n cursor: pager.cursor,\n limit: PAGE_SIZE,\n });\n const rows = sessionsQuery.data?.items ?? [];\n const active = rows.find((s) => s.id === activeId) ?? null;\n\n const courseTitles = useMemo(\n () => new Map((coursesQuery.data?.items ?? []).map((c) => [c.id, c.title] as const)),\n [coursesQuery.data]\n );\n const courseOptions = [\n { value: ALL_COURSES, label: tr('buildmyiq.tutorReview.allCourses', 'All courses') },\n ...(coursesQuery.data?.items ?? []).map((c) => ({ value: c.id, label: c.title })),\n ];\n\n const learnerCell = (learnerId: string) =>\n learnerId === currentUser?.id ? (\n <div className=\"flex items-center gap-2.5\">\n <Avatar size=\"sm\" seed={learnerId} name={currentUser?.name ?? null} />\n <span className=\"font-medium text-text-primary\">{tr('common.you', 'You')}</span>\n </div>\n ) : (\n <div className=\"flex items-center gap-2.5\">\n <Avatar size=\"sm\" seed={learnerId} name={null} />\n <span className=\"font-mono text-xs text-text-tertiary\" title={learnerId}>\n {learnerId.slice(0, 8)}…\n </span>\n </div>\n );\n\n const columns: Column<BuiTutorSession>[] = [\n {\n key: 'learner',\n header: tr('buildmyiq.tutorReview.learner', 'Learner'),\n render: (s) => learnerCell(s.learnerId),\n },\n {\n key: 'course',\n header: tr('buildmyiq.tutorReview.course', 'Course'),\n render: (s) => (\n <span className=\"text-text-primary\" title={s.courseId}>\n {courseTitles.get(s.courseId) ?? `${s.courseId.slice(0, 8)}…`}\n </span>\n ),\n },\n {\n key: 'lesson',\n header: tr('buildmyiq.tutorReview.lesson', 'Lesson'),\n hideOnMobile: true,\n render: (s) => <LessonTitle courseId={s.courseId} lessonId={s.lessonId} />,\n },\n {\n key: 'answers',\n header: tr('buildmyiq.tutorReview.answers', 'Answers'),\n align: 'right',\n sortValue: (s) => summarizeSession(s).answers,\n render: (s) => <span className=\"tabular-nums\">{summarizeSession(s).answers}</span>,\n },\n {\n key: 'reviewed',\n header: tr('buildmyiq.tutorReview.reviewed', 'Reviewed'),\n align: 'right',\n sortValue: (s) => summarizeSession(s).reviewed,\n render: (s) => {\n const { reviewed } = summarizeSession(s);\n return (\n <span\n className={\n reviewed > 0\n ? 'font-medium tabular-nums text-status-success-text'\n : 'tabular-nums text-text-tertiary'\n }\n >\n {reviewed}\n </span>\n );\n },\n },\n {\n key: 'updatedAt',\n header: tr('buildmyiq.tutorReview.updated', 'Last activity'),\n hideOnMobile: true,\n sortValue: (s) => s.updatedAt,\n render: (s) => <span className=\"text-text-secondary\">{formatDateTime(s.updatedAt)}</span>,\n },\n ];\n\n return (\n <PageLayout\n title={tr('buildmyiq.tutorReview.title', 'Tutor review')}\n description={tr(\n 'buildmyiq.tutorReview.description',\n 'Read what the AI tutor told your learners, and annotate, correct or redact any answer. Learners see your reviewed version with a \"reviewed by your instructor\" marker.'\n )}\n >\n <div className=\"mb-5 max-w-sm\">\n <Field\n label={tr('buildmyiq.tutorReview.courseFilter', 'Course')}\n htmlFor=\"tutor-review-course\"\n >\n <SelectField\n id=\"tutor-review-course\"\n value={courseId}\n onChange={(value) => {\n setCourseId(value);\n pager.reset();\n }}\n options={courseOptions}\n />\n </Field>\n </div>\n\n {sessionsQuery.isLoading ? (\n <LoadingView label={tr('buildmyiq.tutorReview.loading', 'Loading tutor conversations…')} />\n ) : sessionsQuery.isError ? (\n <ErrorView\n title={tr('buildmyiq.tutorReview.loadError', \"Couldn't load tutor conversations\")}\n message={sessionsQuery.error instanceof Error ? sessionsQuery.error.message : undefined}\n onRetry={() => sessionsQuery.refetch()}\n />\n ) : rows.length === 0 ? (\n <EmptyState\n icon={<Bot className=\"h-10 w-10\" />}\n title={tr('buildmyiq.tutorReview.empty', 'No tutor conversations yet')}\n description={tr(\n 'buildmyiq.tutorReview.emptyDescription',\n 'Conversations appear here once learners ask the AI tutor about a lesson.'\n )}\n />\n ) : (\n <div className=\"flex flex-col gap-4\">\n <DataTable\n columns={columns}\n rows={rows}\n rowKey={(s) => s.id}\n onRowClick={(s) => setActiveId(s.id)}\n defaultSort={{ key: 'updatedAt', dir: 'desc' }}\n />\n <Pagination\n page={pager.page}\n pageSize={PAGE_SIZE}\n itemCount={rows.length}\n hasMore={sessionsQuery.data?.pageInfo?.hasMore ?? false}\n onPageChange={(page) => pager.onPageChange(page, sessionsQuery.data?.pageInfo?.cursor)}\n />\n </div>\n )}\n\n <Sheet\n open={active != null}\n onOpenChange={(open) => {\n if (!open) setActiveId(null);\n }}\n >\n <SheetContent\n side={isDesktop ? 'right' : 'bottom'}\n className={\n isDesktop\n ? 'flex w-full max-w-none flex-col gap-0 p-0 sm:max-w-xl'\n : 'flex max-h-[92vh] w-full flex-col gap-0 rounded-t-2xl p-0'\n }\n >\n {active && (\n <>\n <SheetHeader className=\"border-b border-border-seam p-5 pr-12\">\n <SheetTitle>\n {courseTitles.get(active.courseId) ??\n tr('buildmyiq.tutorReview.conversation', 'Tutor conversation')}\n </SheetTitle>\n <SheetDescription>\n <LessonTitle courseId={active.courseId} lessonId={active.lessonId} /> ·{' '}\n {formatDateTime(active.updatedAt)}\n </SheetDescription>\n </SheetHeader>\n <div\n className=\"flex-1 overflow-y-auto px-5 py-4\"\n data-testid=\"tutor-review-transcript\"\n >\n <Transcript session={active} onAction={setTarget} />\n </div>\n </>\n )}\n </SheetContent>\n </Sheet>\n\n {target && (\n <ReviewDialog\n key={`${target.message.id}:${target.action}`}\n target={target}\n onClose={() => setTarget(null)}\n />\n )}\n </PageLayout>\n );\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAyDA,IAAM,IAAY,IACZ,IAAc,WAEd,KAA2D,EAAE,aAAU,kBAAe;CAC1F,IAAM,EAAE,YAAS,EAAa,CAAQ,GAChC,IAAQ,GAAM,QAAQ,MAAM,MAAM,EAAE,OAAO,CAAQ,GAAG;CAC5D,OACE,kBAAC,QAAD;EAAM,WAAU;EAAsB,OAAO;YAC1C,KAAS,GAAG,EAAS,MAAM,GAAG,CAAC,EAAE;CAC9B,CAAA;AAEV,GAOM,KAAmE,EAAE,WAAQ,iBAAc;CAC/F,IAAM,IAAK,EAAM,GACX,IAAS,EAAwB,GACjC,EAAE,YAAS,cAAW,GACtB,CAAC,GAAM,KAAW,EAAS,MAAW,UAAU,KAAM,EAAQ,QAAQ,QAAQ,EAAG,GACjF,CAAC,GAAW,KAAgB,EAChC,EAAQ,QAAQ,WAAW,cACvB,EAAQ,UACP,EAAQ,mBAAmB,EAAQ,OAC1C,GACM,IAAQ,EAAiB,EAAQ,IAAI;EAAE;EAAQ;EAAM,kBAAkB;CAAU,CAAC,GAElF,IAA0F;EAC9F,UAAU;GACR,OAAO,EAAG,uCAAuC,iBAAiB;GAClE,aAAa,EACX,6CACA,iEACF;GACA,KAAK,EAAG,kCAAkC,UAAU;EACtD;EACA,SAAS;GACP,OAAO,EAAG,sCAAsC,gBAAgB;GAChE,aAAa,EACX,4CACA,0FACF;GACA,KAAK,EAAG,iCAAiC,SAAS;EACpD;EACA,QAAQ;GACN,OAAO,EAAG,qCAAqC,eAAe;GAC9D,aAAa,EACX,2CACA,0GACF;GACA,KAAK,EAAG,gCAAgC,QAAQ;EAClD;EACA,OAAO;GACL,OAAO,EAAG,oCAAoC,cAAc;GAC5D,aAAa,EACX,0CACA,8EACF;GACA,KAAK,EAAG,+BAA+B,cAAc;EACvD;CACF,GAEM,IAAa,YAA2B;EACvC,MAAM,IACX,IAAI;GAEF,AADA,MAAM,EAAO,YAAY,EAAM,KAAK,GACpC,EAAQ;EACV,QAAQ,CAER;CACF;CAEA,OACE,kBAAC,GAAD;EACE,MAAA;EACS;EACT,OAAO,EAAK,GAAQ;EACpB,aAAa,EAAK,GAAQ;EAC1B,QACE,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD;GAAQ,SAAQ;GAAU,SAAS;aAChC,EAAG,iBAAiB,QAAQ;EACvB,CAAA,GACR,kBAAC,GAAD;GACE,SAAS,MAAW,WAAW,gBAAgB;GAC/C,eAAe,KAAK,EAAW;GAC/B,UAAU,EAAO,aAAa,CAAC,EAAM;GACrC,eAAY;aAEX,EAAK,GAAQ;EACR,CAAA,CACR,EAAA,CAAA;YAGJ,kBAAC,OAAD;GAAK,WAAU;aAAf;IACE,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAG,wCAAwC,iBAAiB;KAC5D,CAAA,GACH,kBAAC,KAAD;MAAG,WAAU;gBACV,EAAQ,mBAAmB,EAAQ;KACnC,CAAA,CACA;;IACJ,MAAW,aACV,kBAAC,GAAD;KACE,OAAO,EAAG,wCAAwC,kBAAkB;KACpE,SAAQ;KACR,UAAA;KACA,MAAM,EAAG,kCAAkC,6BAA6B,EACtE,KAAA,IACF,CAAC;eAED,kBAAC,GAAD;MACE,IAAG;MACH,MAAM;MACN,OAAO;MACP,WAAW,MAAM,EAAa,EAAE,OAAO,KAAK;KAC7C,CAAA;IACI,CAAA;IAER,MAAW,WACV,kBAAC,GAAD;KACE,OAAO,EAAG,mCAAmC,sBAAsB;KACnE,SAAQ;KACR,UAAU,MAAW;KACrB,MAAM,EAAG,kCAAkC,6BAA6B,EACtE,KAAA,IACF,CAAC;eAED,kBAAC,GAAD;MACE,IAAG;MACH,MAAM;MACN,OAAO;MACP,WAAW,MAAM,EAAQ,EAAE,OAAO,KAAK;KACxC,CAAA;IACI,CAAA;IAER,CAAC,EAAM,OAAO,EAAK,KAAK,KAAK,MAAW,cACvC,kBAAC,KAAD;KAAG,WAAU;eAA8B,EAAM;IAAU,CAAA;IAE5D,EAAO,WACN,kBAAC,KAAD;KAAG,WAAU;eACV,EAAO,iBAAiB,QACrB,EAAO,MAAM,UACb,EAAG,mCAAmC,4BAA4B;IACrE,CAAA;GAEF;;CACA,CAAA;AAEX,GAEM,IAAqD;CACzD,UAAU;CACV,SAAS;CACT,QAAQ;CACR,OAAO;AACT,GAEM,KAGA,EAAE,YAAS,kBAAe;CAC9B,IAAM,IAAK,EAAM,GACX,CAAC,GAAa,KAAkB,EAAS,EAAK,GAC9C,IAAS,EAAQ,QAAQ,QACzB,IAAW,MAAW,eAAe,MAAW,YAChD,IAAU,EAAQ,iBAAiB,CAAC;CAE1C,OACE,kBAAC,OAAD;EACE,WAAU;EACV,eAAY;EACZ,mBAAiB,EAAQ;EACzB,sBAAoB,KAAU;YAJhC;GAME,kBAAC,KAAD;IAAG,WAAU;cACV,EAAG,+BAA+B,UAAU;GAC5C,CAAA;GACF,KACC,kBAAC,KAAD;IAAG,WAAU;cACV,EAAG,qCAAqC,cAAc;GACtD,CAAA;GAEL,kBAAC,KAAD;IACE,WACE,MAAW,aACP,6EACA;cAGL,EAAQ;GACR,CAAA;GACF,EAAQ,UAAU,kBAAC,GAAD;IAAmB,QAAQ,EAAQ;IAAQ,UAAS;GAAY,CAAA;GAClF,KAAY,EAAQ,mBAAmB,QACtC,kBAAC,WAAD;IAAS,WAAU;cAAnB,CACE,kBAAC,WAAD;KAAS,WAAU;eAChB,EAAG,wCAAwC,iBAAiB;IACtD,CAAA,GACT,kBAAC,KAAD;KAAG,WAAU;eACV,EAAQ;IACR,CAAA,CACI;;GAGX,kBAAC,GAAD;IAAqB,UAAS;IAAkB,QAAO;IAAS,gBAAe;cAC7E,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,GAAD;OACE,MAAK;OACL,SAAQ;OACR,eAAe,EAAS;QAAE;QAAS,QAAQ;OAAW,CAAC;iBAHzD,CAKE,kBAAC,GAAD,EAAmB,WAAU,eAAgB,CAAA,GAC5C,EAAG,kCAAkC,UAAU,CAC1C;;MACR,kBAAC,GAAD;OACE,MAAK;OACL,SAAQ;OACR,eAAe,EAAS;QAAE;QAAS,QAAQ;OAAU,CAAC;iBAHxD,CAKE,kBAAC,GAAD,EAAY,WAAU,eAAgB,CAAA,GACrC,EAAG,iCAAiC,SAAS,CACxC;;MACR,kBAAC,GAAD;OACE,MAAK;OACL,SAAQ;OACR,WAAU;OACV,eAAe,EAAS;QAAE;QAAS,QAAQ;OAAS,CAAC;iBAJvD,CAME,kBAAC,GAAD,EAAQ,WAAU,eAAgB,CAAA,GACjC,EAAG,gCAAgC,QAAQ,CACtC;;MACP,KACC,kBAAC,GAAD;OACE,MAAK;OACL,SAAQ;OACR,eAAe,EAAS;QAAE;QAAS,QAAQ;OAAQ,CAAC;iBAHtD,CAKE,kBAAC,GAAD,EAAO,WAAU,eAAgB,CAAA,GAChC,EAAG,+BAA+B,cAAc,CAC3C;;KAEP;;GACc,CAAA;GAEpB,EAAQ,SAAS,KAChB,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,UAAD;KACE,MAAK;KACL,WAAU;KACV,eAAe,GAAgB,MAAM,CAAC,CAAC;KACvC,iBAAe;eAJjB,CAME,kBAAC,GAAD,EAAS,WAAU,cAAe,CAAA,GACjC,EAAG,iCAAiC,8BAA8B,EACjE,OAAO,EAAQ,OACjB,CAAC,CACK;QACP,KACC,kBAAC,MAAD;KAAI,WAAU;eACX,EAAQ,KAAK,MACZ,kBAAC,MAAD;MAAmB,WAAU;gBAA7B;OACE,kBAAC,QAAD;QAAM,WAAU;kBACb,EAAG,gCAAgC,EAAM,UAAU,EAAa,EAAM,OAAO;OAC1E,CAAA;OAAE;OAAI;OACT,EAAe,EAAM,SAAS;OAAE;OAAG;OACtC,kBAAC,QAAD;QAAM,WAAU;QAAY,OAAO,EAAM;kBAAzC,CACG,EAAM,WAAW,MAAM,GAAG,CAAC,GAAE,GAC1B;;OACL,EAAM,QAAQ,kBAAC,KAAD;QAAG,WAAU;kBAA8B,EAAM;OAAQ,CAAA;MACtE;QATK,EAAM,EASX,CACL;IACC,CAAA,CAEH;;EAEJ;;AAET,GAEM,KAA0F,EAC9F,YACA,kBACI;CACJ,IAAM,IAAK,EAAM;CAQjB,OAPI,EAAQ,SAAS,WAAW,IAE5B,kBAAC,KAAD;EAAG,WAAU;YACV,EAAG,oCAAoC,wCAAwC;CAC/E,CAAA,IAIL,kBAAC,MAAD;EAAI,WAAU;YACX,EAAQ,SAAS,KAAK,MACrB,EAAQ,SAAS,cACf,kBAAC,MAAD;GAAqB,WAAU;aAA/B,CACE,kBAAC,QAAD;IAAM,WAAU;cACd,kBAAC,GAAD,EAAK,WAAU,0CAA2C,CAAA;GACtD,CAAA,GACN,kBAAC,GAAD;IAAqB;IAAmB;GAAW,CAAA,CACjD;KALK,EAAQ,EAKb,IAEJ,kBAAC,MAAD;GAAqB,WAAU;aAA/B,CACE,kBAAC,GAAD;IAAQ,MAAK;IAAK,MAAM,EAAQ;IAAW,MAAM;GAAO,CAAA,GACxD,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,KAAD;KAAG,WAAU;eACV,EAAG,iCAAiC,SAAS;IAC7C,CAAA,GACH,kBAAC,KAAD;KAAG,WAAU;eACV,EAAQ;IACR,CAAA,CACA;KACH;KAVK,EAAQ,EAUb,CAER;CACE,CAAA;AAER,GAEa,UAA4B;CACvC,IAAM,IAAK,EAAM,GACX,EAAE,mBAAgB,EAAa,GAC/B,IAAY,EAAc,oBAAoB,GAC9C,IAAQ,EAAe,GACvB,CAAC,GAAU,KAAe,EAAiB,CAAW,GACtD,CAAC,GAAU,KAAe,EAAwB,IAAI,GACtD,CAAC,GAAQ,KAAa,EAA8B,IAAI,GAExD,IAAe,EAAc,EAAE,OAAO,IAAI,CAAC,GAC3C,IAAgB,EAA0B;EAC9C,UAAU,MAAa,IAAc,KAAA,IAAY;EACjD,QAAQ,EAAM;EACd,OAAO;CACT,CAAC,GACK,IAAO,EAAc,MAAM,SAAS,CAAC,GACrC,IAAS,EAAK,MAAM,MAAM,EAAE,OAAO,CAAQ,KAAK,MAEhD,IAAe,QACb,IAAI,KAAK,EAAa,MAAM,SAAS,CAAC,GAAG,KAAK,MAAM,CAAC,EAAE,IAAI,EAAE,KAAK,CAAU,CAAC,GACnF,CAAC,EAAa,IAAI,CACpB,GACM,IAAgB,CACpB;EAAE,OAAO;EAAa,OAAO,EAAG,oCAAoC,aAAa;CAAE,GACnF,IAAI,EAAa,MAAM,SAAS,CAAC,GAAG,KAAK,OAAO;EAAE,OAAO,EAAE;EAAI,OAAO,EAAE;CAAM,EAAE,CAClF,GAEM,KAAe,MACnB,MAAc,GAAa,KACzB,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,GAAD;GAAQ,MAAK;GAAK,MAAM;GAAW,MAAM,GAAa,QAAQ;EAAO,CAAA,GACrE,kBAAC,QAAD;GAAM,WAAU;aAAiC,EAAG,cAAc,KAAK;EAAQ,CAAA,CAC5E;MAEL,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,GAAD;GAAQ,MAAK;GAAK,MAAM;GAAW,MAAM;EAAO,CAAA,GAChD,kBAAC,QAAD;GAAM,WAAU;GAAuC,OAAO;aAA9D,CACG,EAAU,MAAM,GAAG,CAAC,GAAE,GACnB;IACH;KAGH,IAAqC;EACzC;GACE,KAAK;GACL,QAAQ,EAAG,iCAAiC,SAAS;GACrD,SAAS,MAAM,EAAY,EAAE,SAAS;EACxC;EACA;GACE,KAAK;GACL,QAAQ,EAAG,gCAAgC,QAAQ;GACnD,SAAS,MACP,kBAAC,QAAD;IAAM,WAAU;IAAoB,OAAO,EAAE;cAC1C,EAAa,IAAI,EAAE,QAAQ,KAAK,GAAG,EAAE,SAAS,MAAM,GAAG,CAAC,EAAE;GACvD,CAAA;EAEV;EACA;GACE,KAAK;GACL,QAAQ,EAAG,gCAAgC,QAAQ;GACnD,cAAc;GACd,SAAS,MAAM,kBAAC,GAAD;IAAa,UAAU,EAAE;IAAU,UAAU,EAAE;GAAW,CAAA;EAC3E;EACA;GACE,KAAK;GACL,QAAQ,EAAG,iCAAiC,SAAS;GACrD,OAAO;GACP,YAAY,MAAM,EAAiB,CAAC,EAAE;GACtC,SAAS,MAAM,kBAAC,QAAD;IAAM,WAAU;cAAgB,EAAiB,CAAC,EAAE;GAAc,CAAA;EACnF;EACA;GACE,KAAK;GACL,QAAQ,EAAG,kCAAkC,UAAU;GACvD,OAAO;GACP,YAAY,MAAM,EAAiB,CAAC,EAAE;GACtC,SAAS,MAAM;IACb,IAAM,EAAE,gBAAa,EAAiB,CAAC;IACvC,OACE,kBAAC,QAAD;KACE,WACE,IAAW,IACP,sDACA;eAGL;IACG,CAAA;GAEV;EACF;EACA;GACE,KAAK;GACL,QAAQ,EAAG,iCAAiC,eAAe;GAC3D,cAAc;GACd,YAAY,MAAM,EAAE;GACpB,SAAS,MAAM,kBAAC,QAAD;IAAM,WAAU;cAAuB,EAAe,EAAE,SAAS;GAAQ,CAAA;EAC1F;CACF;CAEA,OACE,kBAAC,GAAD;EACE,OAAO,EAAG,+BAA+B,cAAc;EACvD,aAAa,EACX,qCACA,0KACF;YALF;GAOE,kBAAC,OAAD;IAAK,WAAU;cACb,kBAAC,GAAD;KACE,OAAO,EAAG,sCAAsC,QAAQ;KACxD,SAAQ;eAER,kBAAC,GAAD;MACE,IAAG;MACH,OAAO;MACP,WAAW,MAAU;OAEnB,AADA,EAAY,CAAK,GACjB,EAAM,MAAM;MACd;MACA,SAAS;KACV,CAAA;IACI,CAAA;GACJ,CAAA;GAEJ,EAAc,YACb,kBAAC,GAAD,EAAa,OAAO,EAAG,iCAAiC,8BAA8B,EAAI,CAAA,IACxF,EAAc,UAChB,kBAAC,GAAD;IACE,OAAO,EAAG,mCAAmC,mCAAmC;IAChF,SAAS,EAAc,iBAAiB,QAAQ,EAAc,MAAM,UAAU,KAAA;IAC9E,eAAe,EAAc,QAAQ;GACtC,CAAA,IACC,EAAK,WAAW,IAClB,kBAAC,GAAD;IACE,MAAM,kBAAC,GAAD,EAAK,WAAU,YAAa,CAAA;IAClC,OAAO,EAAG,+BAA+B,4BAA4B;IACrE,aAAa,EACX,0CACA,0EACF;GACD,CAAA,IAED,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,GAAD;KACW;KACH;KACN,SAAS,MAAM,EAAE;KACjB,aAAa,MAAM,EAAY,EAAE,EAAE;KACnC,aAAa;MAAE,KAAK;MAAa,KAAK;KAAO;IAC9C,CAAA,GACD,kBAAC,GAAD;KACE,MAAM,EAAM;KACZ,UAAU;KACV,WAAW,EAAK;KAChB,SAAS,EAAc,MAAM,UAAU,WAAW;KAClD,eAAe,MAAS,EAAM,aAAa,GAAM,EAAc,MAAM,UAAU,MAAM;IACtF,CAAA,CACE;;GAGP,kBAAC,GAAD;IACE,MAAM,KAAU;IAChB,eAAe,MAAS;KACtB,AAAK,KAAM,EAAY,IAAI;IAC7B;cAEA,kBAAC,GAAD;KACE,MAAM,IAAY,UAAU;KAC5B,WACE,IACI,0DACA;eAGL,KACC,kBAAA,GAAA,EAAA,UAAA,CACE,kBAAC,GAAD;MAAa,WAAU;gBAAvB,CACE,kBAAC,GAAD,EAAA,UACG,EAAa,IAAI,EAAO,QAAQ,KAC/B,EAAG,sCAAsC,oBAAoB,EACrD,CAAA,GACZ,kBAAC,GAAD,EAAA,UAAA;OACE,kBAAC,GAAD;QAAa,UAAU,EAAO;QAAU,UAAU,EAAO;OAAW,CAAA;OAAC;OAAG;OACvE,EAAe,EAAO,SAAS;MAChB,EAAA,CAAA,CACP;SACb,kBAAC,OAAD;MACE,WAAU;MACV,eAAY;gBAEZ,kBAAC,GAAD;OAAY,SAAS;OAAQ,UAAU;MAAY,CAAA;KAChD,CAAA,CACL,EAAA,CAAA;IAEQ,CAAA;GACT,CAAA;GAEN,KACC,kBAAC,GAAD;IAEU;IACR,eAAe,EAAU,IAAI;GAC9B,GAHM,GAAG,EAAO,QAAQ,GAAG,GAAG,EAAO,QAGrC;EAEO;;AAEhB"}
1
+ {"version":3,"file":"TutorReviewPage.js","names":[],"sources":["../../../../src/buildmyiq/pages/tutorReview/TutorReviewPage.tsx"],"sourcesContent":["/**\n * TutorReviewPage — educator review of AI tutor conversations (BOFF-9663).\n * @module buildmyiq/pages/tutorReview\n *\n * Lists the workspace's tutor conversations (`buiListTutorSessions`, EDITOR+),\n * optionally filtered to one course. Opening a conversation shows every turn;\n * for each tutor answer a reviewer can annotate it, correct it, redact it, or\n * clear an earlier review (`buiReviewTutorAnswer`). The original answer is\n * never overwritten: it stays visible here with the full review history, while\n * the learner sees the reviewed version with a \"reviewed by your instructor\"\n * marker.\n */\n\nimport { useMemo, useState, type FC } from 'react';\nimport { Bot, EyeOff, History, MessageSquareText, PencilLine, Undo2 } from 'lucide-react';\nimport {\n Button,\n Sheet,\n SheetContent,\n SheetDescription,\n SheetHeader,\n SheetTitle,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport { useMediaQuery } from '@burdenoff/fe-libs/shared/hooks';\n\nimport {\n Avatar,\n DataTable,\n EmptyState,\n ErrorView,\n LoadingView,\n PageLayout,\n Pagination,\n TutorReviewMarker,\n type Column,\n} from '../../components';\nimport { BuiPermissionButton } from '../../components/BuiPermissionButton';\nimport { useBuildmyiq } from '../../context';\nimport {\n useBuiCourse,\n useBuiCourses,\n useBuiReviewTutorAnswer,\n useBuiTutorReviewSessions,\n useCursorPager,\n} from '../../hooks';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { formatDateTime } from '../../utils/formatters';\nimport type { BuiTutorMessage, BuiTutorReviewAction, BuiTutorSession } from '../../types';\nimport { Field, Modal, SelectField } from '../authoring/_forms';\nimport {\n MAX_CORRECTED_CONTENT_LENGTH,\n MAX_REVIEW_NOTE_LENGTH,\n buildReviewInput,\n summarizeSession,\n} from './tutorReviewModel';\n\nconst PAGE_SIZE = 20;\nconst ALL_COURSES = '__all__';\n\nconst LessonTitle: FC<{ courseId: string; lessonId: string }> = ({ courseId, lessonId }) => {\n const { data } = useBuiCourse(courseId);\n const title = data?.lessons.find((l) => l.id === lessonId)?.title;\n return (\n <span className=\"text-text-secondary\" title={lessonId}>\n {title ?? `${lessonId.slice(0, 8)}…`}\n </span>\n );\n};\n\ninterface ReviewTarget {\n message: BuiTutorMessage;\n action: BuiTutorReviewAction;\n}\n\nconst ReviewDialog: FC<{ target: ReviewTarget; onClose: () => void }> = ({ target, onClose }) => {\n const tr = useTr();\n const review = useBuiReviewTutorAnswer();\n const { message, action } = target;\n const [note, setNote] = useState(action === 'CLEAR' ? '' : (message.review?.note ?? ''));\n const [corrected, setCorrected] = useState(\n message.review?.status === 'CORRECTED'\n ? message.content\n : (message.originalContent ?? message.content)\n );\n const built = buildReviewInput(message.id, { action, note, correctedContent: corrected });\n\n const copy: Record<BuiTutorReviewAction, { title: string; description: string; cta: string }> = {\n ANNOTATE: {\n title: tr('buildmyiq.tutorReview.annotateTitle', 'Annotate answer'),\n description: tr(\n 'buildmyiq.tutorReview.annotateDescription',\n 'The answer stays as it is. The learner sees your note under it.'\n ),\n cta: tr('buildmyiq.tutorReview.annotate', 'Annotate'),\n },\n CORRECT: {\n title: tr('buildmyiq.tutorReview.correctTitle', 'Correct answer'),\n description: tr(\n 'buildmyiq.tutorReview.correctDescription',\n 'The learner sees your corrected answer instead of the original, which is kept for audit.'\n ),\n cta: tr('buildmyiq.tutorReview.correct', 'Correct'),\n },\n REDACT: {\n title: tr('buildmyiq.tutorReview.redactTitle', 'Redact answer'),\n description: tr(\n 'buildmyiq.tutorReview.redactDescription',\n 'The learner no longer sees this answer, and the tutor will not reuse it. The original is kept for audit.'\n ),\n cta: tr('buildmyiq.tutorReview.redact', 'Redact'),\n },\n CLEAR: {\n title: tr('buildmyiq.tutorReview.clearTitle', 'Clear review'),\n description: tr(\n 'buildmyiq.tutorReview.clearDescription',\n 'The learner sees the original answer again. The review stays in the history.'\n ),\n cta: tr('buildmyiq.tutorReview.clear', 'Clear review'),\n },\n };\n\n const handleSave = async (): Promise<void> => {\n if (!built.ok) return;\n try {\n await review.mutateAsync(built.input);\n onClose();\n } catch {\n // Shown inline from `review.error`.\n }\n };\n\n return (\n <Modal\n open\n onClose={onClose}\n title={copy[action].title}\n description={copy[action].description}\n footer={\n <>\n <Button variant=\"outline\" onClick={onClose}>\n {tr('common.cancel', 'Cancel')}\n </Button>\n <Button\n variant={action === 'REDACT' ? 'destructive' : 'default'}\n onClick={() => void handleSave()}\n disabled={review.isPending || !built.ok}\n data-testid=\"tutor-review-save\"\n >\n {copy[action].cta}\n </Button>\n </>\n }\n >\n <div className=\"grid gap-3 py-2\">\n <div className=\"rounded-md border border-border-seam bg-bg-sunken p-3\">\n <p className=\"mb-1 text-xs font-medium text-text-tertiary\">\n {tr('buildmyiq.tutorReview.originalAnswer', 'Original answer')}\n </p>\n <p className=\"max-h-40 overflow-y-auto whitespace-pre-wrap break-words text-sm text-text-secondary\">\n {message.originalContent ?? message.content}\n </p>\n </div>\n {action === 'CORRECT' && (\n <Field\n label={tr('buildmyiq.tutorReview.correctedLabel', 'Corrected answer')}\n htmlFor=\"tutor-review-corrected\"\n required\n hint={tr('buildmyiq.tutorReview.maxChars', 'Up to {{max}} characters.', {\n max: MAX_CORRECTED_CONTENT_LENGTH,\n })}\n >\n <Textarea\n id=\"tutor-review-corrected\"\n rows={6}\n value={corrected}\n onChange={(e) => setCorrected(e.target.value)}\n />\n </Field>\n )}\n {action !== 'CLEAR' && (\n <Field\n label={tr('buildmyiq.tutorReview.noteLabel', 'Note for the learner')}\n htmlFor=\"tutor-review-note\"\n required={action === 'ANNOTATE'}\n hint={tr('buildmyiq.tutorReview.maxChars', 'Up to {{max}} characters.', {\n max: MAX_REVIEW_NOTE_LENGTH,\n })}\n >\n <Textarea\n id=\"tutor-review-note\"\n rows={3}\n value={note}\n onChange={(e) => setNote(e.target.value)}\n />\n </Field>\n )}\n {!built.ok && (note.trim() || action === 'CORRECT') && (\n <p className=\"text-xs text-text-tertiary\">{built.reason}</p>\n )}\n {review.isError && (\n <p className=\"text-sm text-status-error-text\">\n {review.error instanceof Error\n ? review.error.message\n : tr('buildmyiq.tutorReview.saveError', 'Could not save the review.')}\n </p>\n )}\n </div>\n </Modal>\n );\n};\n\nconst ACTION_LABEL: Record<BuiTutorReviewAction, string> = {\n ANNOTATE: 'Annotated',\n CORRECT: 'Corrected',\n REDACT: 'Redacted',\n CLEAR: 'Review cleared',\n};\n\nconst AnswerCard: FC<{\n message: BuiTutorMessage;\n onAction: (target: ReviewTarget) => void;\n}> = ({ message, onAction }) => {\n const tr = useTr();\n const [showHistory, setShowHistory] = useState(false);\n const status = message.review?.status;\n const replaced = status === 'CORRECTED' || status === 'REDACTED';\n const history = message.reviewHistory ?? [];\n\n return (\n <div\n className=\"min-w-0 flex-1\"\n data-testid=\"tutor-review-answer\"\n data-message-id={message.id}\n data-review-status={status ?? 'NONE'}\n >\n <p className=\"text-sm font-semibold text-text-primary\">\n {tr('buildmyiq.tutorReview.tutor', 'AI Tutor')}\n </p>\n {replaced && (\n <p className=\"mt-1 text-xs font-medium text-text-tertiary\">\n {tr('buildmyiq.tutorReview.learnerSees', 'Learner sees')}\n </p>\n )}\n <p\n className={\n status === 'REDACTED'\n ? 'mt-0.5 whitespace-pre-wrap break-words text-sm italic text-text-tertiary'\n : 'mt-0.5 whitespace-pre-wrap break-words text-sm text-text-secondary'\n }\n >\n {message.content}\n </p>\n {message.review && <TutorReviewMarker review={message.review} audience=\"reviewer\" />}\n {replaced && message.originalContent != null && (\n <details className=\"mt-2 rounded-md border border-border-seam px-2.5 py-1.5\">\n <summary className=\"cursor-pointer text-xs font-medium text-text-secondary\">\n {tr('buildmyiq.tutorReview.originalAnswer', 'Original answer')}\n </summary>\n <p className=\"mt-1 whitespace-pre-wrap break-words text-sm text-text-secondary\">\n {message.originalContent}\n </p>\n </details>\n )}\n\n <BuiPermissionButton resource=\"buildmyiq-tutor\" action=\"update\" deniedBehavior=\"hide\">\n <div className=\"mt-2 flex flex-wrap gap-1.5\">\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => onAction({ message, action: 'ANNOTATE' })}\n >\n <MessageSquareText className=\"mr-1 h-4 w-4\" />\n {tr('buildmyiq.tutorReview.annotate', 'Annotate')}\n </Button>\n <Button\n size=\"sm\"\n variant=\"outline\"\n onClick={() => onAction({ message, action: 'CORRECT' })}\n >\n <PencilLine className=\"mr-1 h-4 w-4\" />\n {tr('buildmyiq.tutorReview.correct', 'Correct')}\n </Button>\n <Button\n size=\"sm\"\n variant=\"ghost\"\n className=\"text-status-error-text hover:text-status-error-text\"\n onClick={() => onAction({ message, action: 'REDACT' })}\n >\n <EyeOff className=\"mr-1 h-4 w-4\" />\n {tr('buildmyiq.tutorReview.redact', 'Redact')}\n </Button>\n {status && (\n <Button\n size=\"sm\"\n variant=\"ghost\"\n onClick={() => onAction({ message, action: 'CLEAR' })}\n >\n <Undo2 className=\"mr-1 h-4 w-4\" />\n {tr('buildmyiq.tutorReview.clear', 'Clear review')}\n </Button>\n )}\n </div>\n </BuiPermissionButton>\n\n {history.length > 0 && (\n <div className=\"mt-2\">\n <button\n type=\"button\"\n className=\"inline-flex items-center gap-1 text-xs text-text-secondary hover:text-text-primary\"\n onClick={() => setShowHistory((v) => !v)}\n aria-expanded={showHistory}\n >\n <History className=\"h-3.5 w-3.5\" />\n {tr('buildmyiq.tutorReview.history', 'Review history ({{count}})', {\n count: history.length,\n })}\n </button>\n {showHistory && (\n <ol className=\"mt-1.5 grid gap-1.5 border-l border-border-seam pl-3\">\n {history.map((entry) => (\n <li key={entry.id} className=\"text-xs text-text-secondary\">\n <span className=\"font-medium text-text-primary\">\n {tr(`buildmyiq.tutorReview.action.${entry.action}`, ACTION_LABEL[entry.action])}\n </span>{' '}\n · {formatDateTime(entry.createdAt)} ·{' '}\n <span className=\"font-mono\" title={entry.reviewerId}>\n {entry.reviewerId.slice(0, 8)}…\n </span>\n {entry.note && <p className=\"mt-0.5 whitespace-pre-wrap\">{entry.note}</p>}\n </li>\n ))}\n </ol>\n )}\n </div>\n )}\n </div>\n );\n};\n\nconst Transcript: FC<{ session: BuiTutorSession; onAction: (target: ReviewTarget) => void }> = ({\n session,\n onAction,\n}) => {\n const tr = useTr();\n if (session.messages.length === 0) {\n return (\n <p className=\"text-sm text-text-secondary\">\n {tr('buildmyiq.tutorReview.noMessages', 'This conversation has no messages yet.')}\n </p>\n );\n }\n return (\n <ul className=\"flex flex-col gap-5\">\n {session.messages.map((message) =>\n message.role === 'ASSISTANT' ? (\n <li key={message.id} className=\"flex gap-3\">\n <span className=\"flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-[var(--color-accent-soft)]\">\n <Bot className=\"h-4 w-4 text-[var(--color-accent-teal)]\" />\n </span>\n <AnswerCard message={message} onAction={onAction} />\n </li>\n ) : (\n <li key={message.id} className=\"flex gap-3\">\n <Avatar size=\"sm\" seed={session.learnerId} name={null} />\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-semibold text-text-primary\">\n {tr('buildmyiq.tutorReview.learner', 'Learner')}\n </p>\n <p className=\"mt-0.5 whitespace-pre-wrap break-words text-sm text-text-secondary\">\n {message.content}\n </p>\n </div>\n </li>\n )\n )}\n </ul>\n );\n};\n\nexport const TutorReviewPage: FC = () => {\n const tr = useTr();\n const { currentUser } = useBuildmyiq();\n const isDesktop = useMediaQuery('(min-width: 768px)');\n const pager = useCursorPager();\n const [courseId, setCourseId] = useState<string>(ALL_COURSES);\n const [activeId, setActiveId] = useState<string | null>(null);\n const [target, setTarget] = useState<ReviewTarget | null>(null);\n\n const coursesQuery = useBuiCourses({ limit: 100 });\n const sessionsQuery = useBuiTutorReviewSessions({\n courseId: courseId === ALL_COURSES ? undefined : courseId,\n cursor: pager.cursor,\n limit: PAGE_SIZE,\n });\n const rows = sessionsQuery.data?.items ?? [];\n const active = rows.find((s) => s.id === activeId) ?? null;\n\n const courseTitles = useMemo(\n () => new Map((coursesQuery.data?.items ?? []).map((c) => [c.id, c.title] as const)),\n [coursesQuery.data]\n );\n const courseOptions = [\n { value: ALL_COURSES, label: tr('buildmyiq.tutorReview.allCourses', 'All courses') },\n ...(coursesQuery.data?.items ?? []).map((c) => ({ value: c.id, label: c.title })),\n ];\n\n const learnerCell = (learnerId: string) =>\n learnerId === currentUser?.id ? (\n <div className=\"flex items-center gap-2.5\">\n <Avatar size=\"sm\" seed={learnerId} name={currentUser?.name ?? null} />\n <span className=\"font-medium text-text-primary\">{tr('common.you', 'You')}</span>\n </div>\n ) : (\n <div className=\"flex items-center gap-2.5\">\n <Avatar size=\"sm\" seed={learnerId} name={null} />\n <span className=\"font-mono text-xs text-text-tertiary\" title={learnerId}>\n {learnerId.slice(0, 8)}…\n </span>\n </div>\n );\n\n const columns: Column<BuiTutorSession>[] = [\n {\n key: 'learner',\n header: tr('buildmyiq.tutorReview.learner', 'Learner'),\n render: (s) => learnerCell(s.learnerId),\n },\n {\n key: 'course',\n header: tr('buildmyiq.tutorReview.course', 'Course'),\n render: (s) => (\n <span className=\"text-text-primary\" title={s.courseId}>\n {courseTitles.get(s.courseId) ?? `${s.courseId.slice(0, 8)}…`}\n </span>\n ),\n },\n {\n key: 'lesson',\n header: tr('buildmyiq.tutorReview.lesson', 'Lesson'),\n hideOnMobile: true,\n render: (s) => <LessonTitle courseId={s.courseId} lessonId={s.lessonId} />,\n },\n {\n key: 'answers',\n header: tr('buildmyiq.tutorReview.answers', 'Answers'),\n align: 'right',\n sortValue: (s) => summarizeSession(s).answers,\n render: (s) => <span className=\"tabular-nums\">{summarizeSession(s).answers}</span>,\n },\n {\n key: 'reviewed',\n header: tr('buildmyiq.tutorReview.reviewed', 'Reviewed'),\n align: 'right',\n sortValue: (s) => summarizeSession(s).reviewed,\n render: (s) => {\n const { reviewed } = summarizeSession(s);\n return (\n <span\n className={\n reviewed > 0\n ? 'font-medium tabular-nums text-status-success-text'\n : 'tabular-nums text-text-tertiary'\n }\n >\n {reviewed}\n </span>\n );\n },\n },\n {\n key: 'updatedAt',\n header: tr('buildmyiq.tutorReview.updated', 'Last activity'),\n hideOnMobile: true,\n sortValue: (s) => s.updatedAt,\n render: (s) => <span className=\"text-text-secondary\">{formatDateTime(s.updatedAt)}</span>,\n },\n ];\n\n return (\n <PageLayout\n title={tr('buildmyiq.tutorReview.title', 'Tutor review')}\n description={tr(\n 'buildmyiq.tutorReview.description',\n 'Read what the AI tutor told your learners, and annotate, correct or redact any answer. Learners see your reviewed version with a \"reviewed by your instructor\" marker.'\n )}\n >\n <div className=\"mb-5 max-w-sm\">\n <Field\n label={tr('buildmyiq.tutorReview.courseFilter', 'Course')}\n htmlFor=\"tutor-review-course\"\n >\n <SelectField\n id=\"tutor-review-course\"\n value={courseId}\n onChange={(value) => {\n setCourseId(value);\n pager.reset();\n }}\n options={courseOptions}\n />\n </Field>\n </div>\n\n {sessionsQuery.isLoading ? (\n <LoadingView label={tr('buildmyiq.tutorReview.loading', 'Loading tutor conversations…')} />\n ) : sessionsQuery.isError ? (\n <ErrorView\n title={tr('buildmyiq.tutorReview.loadError', \"Couldn't load tutor conversations\")}\n message={sessionsQuery.error instanceof Error ? sessionsQuery.error.message : undefined}\n onRetry={() => sessionsQuery.refetch()}\n />\n ) : rows.length === 0 ? (\n <EmptyState\n icon={<Bot className=\"h-10 w-10\" />}\n title={tr('buildmyiq.tutorReview.empty', 'No tutor conversations yet')}\n description={tr(\n 'buildmyiq.tutorReview.emptyDescription',\n 'Conversations appear here once learners ask the AI tutor about a lesson.'\n )}\n />\n ) : (\n <div className=\"flex flex-col gap-4\">\n <DataTable\n columns={columns}\n rows={rows}\n rowKey={(s) => s.id}\n onRowClick={(s) => setActiveId(s.id)}\n defaultSort={{ key: 'updatedAt', dir: 'desc' }}\n />\n <Pagination\n page={pager.page}\n pageSize={PAGE_SIZE}\n itemCount={rows.length}\n hasMore={sessionsQuery.data?.pageInfo?.hasMore ?? false}\n onPageChange={(page) => pager.onPageChange(page, sessionsQuery.data?.pageInfo?.cursor)}\n />\n </div>\n )}\n\n {/*\n The transcript sheet steps aside while a review dialog is open and\n comes back (with the refreshed transcript) when it closes. Two modal\n layers must not be stacked: the sheet overlay sits above the desktop\n dialog, so it would swallow every click on the dialog's buttons, and\n two focus traps would fight over keyboard focus.\n */}\n <Sheet\n open={active != null && target == null}\n onOpenChange={(open) => {\n if (!open && target == null) setActiveId(null);\n }}\n >\n <SheetContent\n side={isDesktop ? 'right' : 'bottom'}\n className={\n isDesktop\n ? 'flex w-full max-w-none flex-col gap-0 p-0 sm:max-w-xl'\n : 'flex max-h-[92vh] w-full flex-col gap-0 rounded-t-2xl p-0'\n }\n >\n {active && (\n <>\n <SheetHeader className=\"border-b border-border-seam p-5 pr-12\">\n <SheetTitle>\n {courseTitles.get(active.courseId) ??\n tr('buildmyiq.tutorReview.conversation', 'Tutor conversation')}\n </SheetTitle>\n <SheetDescription>\n <LessonTitle courseId={active.courseId} lessonId={active.lessonId} /> ·{' '}\n {formatDateTime(active.updatedAt)}\n </SheetDescription>\n </SheetHeader>\n <div\n className=\"flex-1 overflow-y-auto px-5 py-4\"\n data-testid=\"tutor-review-transcript\"\n >\n <Transcript session={active} onAction={setTarget} />\n </div>\n </>\n )}\n </SheetContent>\n </Sheet>\n\n {target && (\n <ReviewDialog\n key={`${target.message.id}:${target.action}`}\n target={target}\n onClose={() => setTarget(null)}\n />\n )}\n </PageLayout>\n );\n};\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@burdenoff/microfe-buildmyiq",
3
- "version": "2026.1028.2",
3
+ "version": "2026.1029.2",
4
4
  "description": "Burdenoff BuildMyIQ Microfrontend — education, learning & development",
5
5
  "type": "module",
6
6
  "files": [