analytica-frontend-lib 1.7.21 → 1.7.23

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 (384) hide show
  1. package/dist/AccessReportModal/index.js +3 -3
  2. package/dist/AccessReportModal/index.mjs +2 -2
  3. package/dist/ActivitiesHistory/index.js +49 -49
  4. package/dist/ActivitiesHistory/index.mjs +48 -48
  5. package/dist/ActivityCardQuestionBanks/index.js +49 -49
  6. package/dist/ActivityCardQuestionBanks/index.mjs +48 -48
  7. package/dist/ActivityCardQuestionPreview/index.js +49 -49
  8. package/dist/ActivityCardQuestionPreview/index.mjs +48 -48
  9. package/dist/ActivityCreate/ActivityCreateContent/index.js +49 -49
  10. package/dist/ActivityCreate/ActivityCreateContent/index.mjs +48 -48
  11. package/dist/ActivityCreate/index.js +49 -49
  12. package/dist/ActivityCreate/index.mjs +48 -48
  13. package/dist/ActivityDetails/index.js +49 -49
  14. package/dist/ActivityDetails/index.mjs +48 -48
  15. package/dist/ActivityFilters/index.js +49 -49
  16. package/dist/ActivityFilters/index.mjs +48 -48
  17. package/dist/ActivityPageLayout/index.js +49 -49
  18. package/dist/ActivityPageLayout/index.mjs +48 -48
  19. package/dist/ActivityPreview/index.js +49 -49
  20. package/dist/ActivityPreview/index.mjs +48 -48
  21. package/dist/AlertManager/index.js +49 -49
  22. package/dist/AlertManager/index.mjs +48 -48
  23. package/dist/AlertManagerView/index.js +3 -3
  24. package/dist/AlertManagerView/index.mjs +2 -2
  25. package/dist/AnnouncementModal/index.js +3 -3
  26. package/dist/AnnouncementModal/index.mjs +2 -2
  27. package/dist/AnnouncementsGate/index.js +4 -4
  28. package/dist/AnnouncementsGate/index.mjs +3 -3
  29. package/dist/AnswerKeyModal/index.js +3 -3
  30. package/dist/AnswerKeyModal/index.mjs +2 -2
  31. package/dist/AppHeader/index.js +6 -6
  32. package/dist/AppHeader/index.mjs +5 -5
  33. package/dist/CalendarCard/index.js +4 -4
  34. package/dist/CalendarCard/index.mjs +3 -3
  35. package/dist/Chat/index.js +3 -3
  36. package/dist/Chat/index.mjs +2 -2
  37. package/dist/CheckBox/index.d.ts.map +1 -1
  38. package/dist/CheckBox/index.js +2 -2
  39. package/dist/CheckBox/index.mjs +1 -1
  40. package/dist/CheckBoxGroup/index.js +49 -49
  41. package/dist/CheckBoxGroup/index.mjs +48 -48
  42. package/dist/ComparatorChart/ComparatorView/index.js +3 -3
  43. package/dist/ComparatorChart/ComparatorView/index.mjs +2 -2
  44. package/dist/CorrectActivityModal/index.js +7 -7
  45. package/dist/CorrectActivityModal/index.mjs +6 -6
  46. package/dist/DownloadButton/index.js +4 -4
  47. package/dist/DownloadButton/index.mjs +3 -3
  48. package/dist/DownloadModal/index.js +3 -3
  49. package/dist/DownloadModal/index.mjs +2 -2
  50. package/dist/DropdownMenu/index.js +3 -3
  51. package/dist/DropdownMenu/index.mjs +2 -2
  52. package/dist/EnemClassroomStartModal/index.d.ts.map +1 -1
  53. package/dist/EnemClassroomStartModal/index.js +7 -7
  54. package/dist/EnemClassroomStartModal/index.mjs +6 -6
  55. package/dist/EnemMoment/index.js +57 -57
  56. package/dist/EnemMoment/index.mjs +51 -51
  57. package/dist/EnemMomentStudentModal/index.js +50 -50
  58. package/dist/EnemMomentStudentModal/index.mjs +48 -48
  59. package/dist/ExamDetailsLayout/ExamDetailsPage/index.js +49 -49
  60. package/dist/ExamDetailsLayout/ExamDetailsPage/index.mjs +48 -48
  61. package/dist/ExamPageLayout/index.js +49 -49
  62. package/dist/ExamPageLayout/index.mjs +48 -48
  63. package/dist/Forum/index.js +5 -5
  64. package/dist/Forum/index.mjs +4 -4
  65. package/dist/KnowledgePerformanceCard/index.js +51 -51
  66. package/dist/KnowledgePerformanceCard/index.mjs +50 -50
  67. package/dist/LessonCardPreview/index.js +49 -49
  68. package/dist/LessonCardPreview/index.mjs +48 -48
  69. package/dist/LessonMediaSections/index.js +49 -49
  70. package/dist/LessonMediaSections/index.mjs +48 -48
  71. package/dist/LessonsCatalog/index.js +49 -49
  72. package/dist/LessonsCatalog/index.mjs +48 -48
  73. package/dist/Modal/index.d.ts.map +1 -1
  74. package/dist/Modal/index.js +2 -2
  75. package/dist/Modal/index.mjs +1 -1
  76. package/dist/MultiCheckSelect/index.js +5 -5
  77. package/dist/MultiCheckSelect/index.mjs +4 -4
  78. package/dist/MultiSearchSelect/index.js +3 -3
  79. package/dist/MultiSearchSelect/index.mjs +2 -2
  80. package/dist/MultipleChoice/index.js +3 -3
  81. package/dist/MultipleChoice/index.mjs +2 -2
  82. package/dist/NotificationCard/index.js +4 -4
  83. package/dist/NotificationCard/index.mjs +3 -3
  84. package/dist/PerformanceReportModal/index.js +3 -3
  85. package/dist/PerformanceReportModal/index.mjs +2 -2
  86. package/dist/PrintableUsersTable/index.js +49 -49
  87. package/dist/PrintableUsersTable/index.mjs +48 -48
  88. package/dist/ProgressModal/index.js +3 -3
  89. package/dist/ProgressModal/index.mjs +2 -2
  90. package/dist/QuestionsPerformanceCard/index.js +50 -50
  91. package/dist/QuestionsPerformanceCard/index.mjs +49 -49
  92. package/dist/Quiz/index.js +6 -6
  93. package/dist/Quiz/index.mjs +5 -5
  94. package/dist/RecommendedLessonCreate/index.js +49 -49
  95. package/dist/RecommendedLessonCreate/index.mjs +48 -48
  96. package/dist/RecommendedLessonDetails/index.js +7 -7
  97. package/dist/RecommendedLessonDetails/index.mjs +6 -6
  98. package/dist/RecommendedLessonsHistory/index.js +49 -49
  99. package/dist/RecommendedLessonsHistory/index.mjs +48 -48
  100. package/dist/ReportDetailModal/index.js +4 -4
  101. package/dist/ReportDetailModal/index.mjs +3 -3
  102. package/dist/RichEditor/index.js +5 -5
  103. package/dist/RichEditor/index.mjs +4 -4
  104. package/dist/{RichEditorCore-6MTYSSKS.mjs → RichEditorCore-2S5YUAOI.mjs} +5 -5
  105. package/dist/{RichEditorCore-KDARBEK4.js → RichEditorCore-N7A2445B.js} +16 -16
  106. package/dist/{RichEditorCore-KDARBEK4.js.map → RichEditorCore-N7A2445B.js.map} +1 -1
  107. package/dist/Search/index.js +4 -4
  108. package/dist/Search/index.mjs +3 -3
  109. package/dist/SendActivityModal/SendActivityModal.js +49 -49
  110. package/dist/SendActivityModal/SendActivityModal.mjs +48 -48
  111. package/dist/SendActivityModal/index.js +49 -49
  112. package/dist/SendActivityModal/index.mjs +48 -48
  113. package/dist/SendLessonModal/index.js +49 -49
  114. package/dist/SendLessonModal/index.mjs +48 -48
  115. package/dist/SimulatedPerformance/SimulatedPerformanceView/index.js +49 -49
  116. package/dist/SimulatedPerformance/SimulatedPerformanceView/index.mjs +48 -48
  117. package/dist/SimulatedPerformance/useSimulatedPerformance/index.js +5 -5
  118. package/dist/SimulatedPerformance/useSimulatedPerformance/index.mjs +4 -4
  119. package/dist/SimulatedStudentSimulationsModal/index.js +6 -6
  120. package/dist/SimulatedStudentSimulationsModal/index.mjs +5 -5
  121. package/dist/SimulationsPage/index.js +49 -49
  122. package/dist/SimulationsPage/index.mjs +48 -48
  123. package/dist/StudentLessonProgressModal/index.js +3 -3
  124. package/dist/StudentLessonProgressModal/index.mjs +2 -2
  125. package/dist/StudentPerformanceDetailsModal/index.js +5 -5
  126. package/dist/StudentPerformanceDetailsModal/index.mjs +4 -4
  127. package/dist/Support/TicketModal/index.js +3 -3
  128. package/dist/Support/TicketModal/index.mjs +2 -2
  129. package/dist/Support/index.js +4 -4
  130. package/dist/Support/index.mjs +3 -3
  131. package/dist/SupportChatComposer/index.js +3 -3
  132. package/dist/SupportChatComposer/index.mjs +2 -2
  133. package/dist/SupportChatEvent/index.js +3 -3
  134. package/dist/SupportChatEvent/index.mjs +2 -2
  135. package/dist/SupportChatMessage/index.js +3 -3
  136. package/dist/SupportChatMessage/index.mjs +2 -2
  137. package/dist/SupportChatWidget/index.js +3 -3
  138. package/dist/SupportChatWidget/index.mjs +2 -2
  139. package/dist/SupportConcludeModal/index.js +4 -4
  140. package/dist/SupportConcludeModal/index.mjs +3 -3
  141. package/dist/SupportConversationHeader/index.js +4 -4
  142. package/dist/SupportConversationHeader/index.mjs +3 -3
  143. package/dist/SupportConversationMessages/index.js +5 -5
  144. package/dist/SupportConversationMessages/index.mjs +4 -4
  145. package/dist/SupportTicketConversation/index.js +12 -12
  146. package/dist/SupportTicketConversation/index.mjs +11 -11
  147. package/dist/TableProvider/index.js +49 -49
  148. package/dist/TableProvider/index.mjs +48 -48
  149. package/dist/UnifiedDraftModelPage/index.js +49 -49
  150. package/dist/UnifiedDraftModelPage/index.mjs +48 -48
  151. package/dist/UnifiedHistoryPage/index.js +49 -49
  152. package/dist/UnifiedHistoryPage/index.mjs +48 -48
  153. package/dist/VideoPlayer/index.js +5 -5
  154. package/dist/VideoPlayer/index.mjs +4 -4
  155. package/dist/Whiteboard/index.js +3 -3
  156. package/dist/Whiteboard/index.mjs +2 -2
  157. package/dist/{chunk-B6QOBHZM.mjs → chunk-24BEZK43.mjs} +2 -2
  158. package/dist/{chunk-CYSODZGX.js → chunk-27Y6V3GM.js} +169 -66
  159. package/dist/chunk-27Y6V3GM.js.map +1 -0
  160. package/dist/{chunk-OQTXTDSK.mjs → chunk-2OJ3QSLU.mjs} +3 -3
  161. package/dist/{chunk-VDUO65VK.mjs → chunk-3EGJJDIM.mjs} +2 -2
  162. package/dist/{chunk-VDUO65VK.mjs.map → chunk-3EGJJDIM.mjs.map} +1 -1
  163. package/dist/{chunk-4OUBPEVS.mjs → chunk-3HKLGNUE.mjs} +3 -3
  164. package/dist/{chunk-YA7XLH7D.js → chunk-3MFACEZ2.js} +3 -3
  165. package/dist/{chunk-YA7XLH7D.js.map → chunk-3MFACEZ2.js.map} +1 -1
  166. package/dist/{chunk-IHBWJM4C.mjs → chunk-3Q7RJKUP.mjs} +2 -2
  167. package/dist/{chunk-PY6WD6SB.mjs → chunk-3WRB4WCD.mjs} +4 -4
  168. package/dist/{chunk-4RHCBMMP.js → chunk-46VHAL4S.js} +3 -3
  169. package/dist/{chunk-4RHCBMMP.js.map → chunk-46VHAL4S.js.map} +1 -1
  170. package/dist/{chunk-POT3ALY3.js → chunk-5DINXU6W.js} +1 -1
  171. package/dist/{chunk-POT3ALY3.js.map → chunk-5DINXU6W.js.map} +1 -1
  172. package/dist/{chunk-L52NFNFK.mjs → chunk-5VC4BK65.mjs} +3 -3
  173. package/dist/{chunk-B67RLIUM.js → chunk-72D7G6YQ.js} +3 -3
  174. package/dist/{chunk-B67RLIUM.js.map → chunk-72D7G6YQ.js.map} +1 -1
  175. package/dist/{chunk-C65DG2RI.mjs → chunk-7FSBWUOM.mjs} +2 -2
  176. package/dist/{chunk-QWMFWYII.js → chunk-7IZNLARX.js} +3 -3
  177. package/dist/{chunk-QWMFWYII.js.map → chunk-7IZNLARX.js.map} +1 -1
  178. package/dist/{chunk-ZCBT2BUZ.js → chunk-7RFHXFFV.js} +13 -13
  179. package/dist/{chunk-ZCBT2BUZ.js.map → chunk-7RFHXFFV.js.map} +1 -1
  180. package/dist/{chunk-EMWX6IKP.js → chunk-7VJP5D26.js} +13 -13
  181. package/dist/{chunk-EMWX6IKP.js.map → chunk-7VJP5D26.js.map} +1 -1
  182. package/dist/{chunk-HCQJR5CE.mjs → chunk-7ZWEBNFB.mjs} +2 -2
  183. package/dist/{chunk-3RHNJUXU.mjs → chunk-ACSEJOW6.mjs} +2 -2
  184. package/dist/{chunk-WQZHUW7C.mjs → chunk-ANFHLSDB.mjs} +3 -3
  185. package/dist/{chunk-H6NQOLQY.mjs → chunk-ANFLWCVD.mjs} +2 -2
  186. package/dist/{chunk-BKBDQXZX.js → chunk-AOYPUZRO.js} +3 -3
  187. package/dist/{chunk-BKBDQXZX.js.map → chunk-AOYPUZRO.js.map} +1 -1
  188. package/dist/{chunk-5WS6IENQ.mjs → chunk-AU2HLQDJ.mjs} +2 -2
  189. package/dist/{chunk-P6FX3NT6.mjs → chunk-AZGDHAP5.mjs} +2 -2
  190. package/dist/{chunk-NCGO7FPB.js → chunk-AZKNYREH.js} +8 -8
  191. package/dist/{chunk-NCGO7FPB.js.map → chunk-AZKNYREH.js.map} +1 -1
  192. package/dist/{chunk-R6MTPYYW.js → chunk-AZTBNERQ.js} +3 -3
  193. package/dist/{chunk-R6MTPYYW.js.map → chunk-AZTBNERQ.js.map} +1 -1
  194. package/dist/{chunk-NPB2JTYC.mjs → chunk-BO3QVQ37.mjs} +4 -4
  195. package/dist/{chunk-2WI4Q2I2.mjs → chunk-BPMWRFSI.mjs} +4 -4
  196. package/dist/{chunk-TEW33GLR.mjs → chunk-BXN3VO6I.mjs} +2 -2
  197. package/dist/{chunk-7ZB3YHF2.mjs → chunk-C6LC5LD5.mjs} +1 -1
  198. package/dist/{chunk-7ZB3YHF2.mjs.map → chunk-C6LC5LD5.mjs.map} +1 -1
  199. package/dist/{chunk-5HONWYQA.js → chunk-C7ILTLT5.js} +3 -3
  200. package/dist/{chunk-5HONWYQA.js.map → chunk-C7ILTLT5.js.map} +1 -1
  201. package/dist/{chunk-OES54VU6.mjs → chunk-CFV2OZ3P.mjs} +116 -13
  202. package/dist/chunk-CFV2OZ3P.mjs.map +1 -0
  203. package/dist/{chunk-4OVIHZ3I.js → chunk-CGXWWPEA.js} +6 -6
  204. package/dist/{chunk-4OVIHZ3I.js.map → chunk-CGXWWPEA.js.map} +1 -1
  205. package/dist/{chunk-MNU45FM3.js → chunk-COT4KW3H.js} +2 -2
  206. package/dist/{chunk-MNU45FM3.js.map → chunk-COT4KW3H.js.map} +1 -1
  207. package/dist/{chunk-JDQUXG75.js → chunk-CVDNCU6L.js} +3 -3
  208. package/dist/{chunk-JDQUXG75.js.map → chunk-CVDNCU6L.js.map} +1 -1
  209. package/dist/{chunk-HBZHWSMZ.mjs → chunk-D46PJSQ7.mjs} +13 -13
  210. package/dist/{chunk-ARYIJ4RU.js → chunk-DDORKDQO.js} +3 -3
  211. package/dist/{chunk-ARYIJ4RU.js.map → chunk-DDORKDQO.js.map} +1 -1
  212. package/dist/{chunk-7YQTDJFA.js → chunk-DKXSR2V3.js} +3 -3
  213. package/dist/{chunk-7YQTDJFA.js.map → chunk-DKXSR2V3.js.map} +1 -1
  214. package/dist/{chunk-I6EAPPBL.mjs → chunk-DZMYIOKZ.mjs} +5 -5
  215. package/dist/{chunk-SKM7NFPY.js → chunk-EPXAFJVW.js} +6 -6
  216. package/dist/{chunk-SKM7NFPY.js.map → chunk-EPXAFJVW.js.map} +1 -1
  217. package/dist/{chunk-AYYMS23L.js → chunk-ESIQ2GB4.js} +7 -7
  218. package/dist/{chunk-AYYMS23L.js.map → chunk-ESIQ2GB4.js.map} +1 -1
  219. package/dist/{chunk-NU2WNYOD.js → chunk-EW7HGUFV.js} +87 -87
  220. package/dist/{chunk-NU2WNYOD.js.map → chunk-EW7HGUFV.js.map} +1 -1
  221. package/dist/{chunk-I4QGQMXI.mjs → chunk-EXYXZFNK.mjs} +2 -2
  222. package/dist/{chunk-RBJAO57A.js → chunk-FAAR4XC5.js} +3 -3
  223. package/dist/{chunk-RBJAO57A.js.map → chunk-FAAR4XC5.js.map} +1 -1
  224. package/dist/{chunk-N2VNE3ON.js → chunk-FDTDJDZI.js} +13 -13
  225. package/dist/{chunk-N2VNE3ON.js.map → chunk-FDTDJDZI.js.map} +1 -1
  226. package/dist/{chunk-SU5Q64TW.mjs → chunk-FVCTKS33.mjs} +6 -5
  227. package/dist/chunk-FVCTKS33.mjs.map +1 -0
  228. package/dist/{chunk-AVHZHWI3.js → chunk-GBIFSFWJ.js} +15 -15
  229. package/dist/{chunk-AVHZHWI3.js.map → chunk-GBIFSFWJ.js.map} +1 -1
  230. package/dist/{chunk-KSUPPAKH.js → chunk-GIQSHTB5.js} +9 -9
  231. package/dist/{chunk-KSUPPAKH.js.map → chunk-GIQSHTB5.js.map} +1 -1
  232. package/dist/{chunk-4QBKD4JN.js → chunk-GUUJYGXF.js} +3 -3
  233. package/dist/{chunk-4QBKD4JN.js.map → chunk-GUUJYGXF.js.map} +1 -1
  234. package/dist/{chunk-M4O46VDY.mjs → chunk-GX2PKLN2.mjs} +2 -2
  235. package/dist/{chunk-5BUTINIM.mjs → chunk-HLVLSWKD.mjs} +3 -3
  236. package/dist/{chunk-FU23WOTY.js → chunk-HSV5VZ4Z.js} +6 -6
  237. package/dist/{chunk-FU23WOTY.js.map → chunk-HSV5VZ4Z.js.map} +1 -1
  238. package/dist/{chunk-MMS2ELDK.mjs → chunk-IIQCS3A2.mjs} +2 -2
  239. package/dist/{chunk-NKWLCZUZ.mjs → chunk-JGGUMHM6.mjs} +2 -2
  240. package/dist/{chunk-HT2GREFH.js → chunk-JLD46DL7.js} +11 -11
  241. package/dist/{chunk-HT2GREFH.js.map → chunk-JLD46DL7.js.map} +1 -1
  242. package/dist/{chunk-NVL5TKWD.mjs → chunk-JYFLOQYT.mjs} +2 -2
  243. package/dist/{chunk-DWGDEUEW.mjs → chunk-KG563P7F.mjs} +5 -5
  244. package/dist/{chunk-TCIC5556.mjs → chunk-KH4TEDGD.mjs} +2 -2
  245. package/dist/{chunk-23PAYKWB.js → chunk-KM27ZDHZ.js} +2 -2
  246. package/dist/{chunk-23PAYKWB.js.map → chunk-KM27ZDHZ.js.map} +1 -1
  247. package/dist/{chunk-XZUMD3CB.mjs → chunk-LODDYDTP.mjs} +2 -2
  248. package/dist/{chunk-XZUMD3CB.mjs.map → chunk-LODDYDTP.mjs.map} +1 -1
  249. package/dist/{chunk-E6CGJBID.mjs → chunk-LUHKMY56.mjs} +2 -2
  250. package/dist/{chunk-TVLEUNKE.mjs → chunk-LZP5VZCO.mjs} +2 -2
  251. package/dist/{chunk-EHNEBXS7.mjs → chunk-M6DXZ2FM.mjs} +2 -2
  252. package/dist/{chunk-B4C5DWAP.mjs → chunk-MMMJDO75.mjs} +2 -2
  253. package/dist/{chunk-AFCWMJQO.js → chunk-MOUPNP53.js} +3 -3
  254. package/dist/{chunk-AFCWMJQO.js.map → chunk-MOUPNP53.js.map} +1 -1
  255. package/dist/{chunk-36KFIZ43.js → chunk-MYPUZ6EG.js} +3 -3
  256. package/dist/{chunk-36KFIZ43.js.map → chunk-MYPUZ6EG.js.map} +1 -1
  257. package/dist/{chunk-6JHIKIPA.js → chunk-NUGLTWEA.js} +23 -23
  258. package/dist/{chunk-6JHIKIPA.js.map → chunk-NUGLTWEA.js.map} +1 -1
  259. package/dist/{chunk-KWXOYHYK.mjs → chunk-P7MORXSE.mjs} +2 -2
  260. package/dist/{chunk-YG4IZ5Q3.mjs → chunk-Q2OSEROT.mjs} +2 -2
  261. package/dist/{chunk-L5SXG6T6.mjs → chunk-QQKOHKO2.mjs} +3 -3
  262. package/dist/{chunk-SADSJ2MV.js → chunk-QVGUYPXO.js} +3 -3
  263. package/dist/{chunk-SADSJ2MV.js.map → chunk-QVGUYPXO.js.map} +1 -1
  264. package/dist/{chunk-K45PTE7F.mjs → chunk-RBKQXT73.mjs} +4 -4
  265. package/dist/{chunk-JZFISWTP.js → chunk-RIN2IN7E.js} +2 -2
  266. package/dist/{chunk-JZFISWTP.js.map → chunk-RIN2IN7E.js.map} +1 -1
  267. package/dist/{chunk-BXLAOYAG.mjs → chunk-RIUVPTG7.mjs} +2 -2
  268. package/dist/{chunk-QVB54LBS.mjs → chunk-RNZGX6MX.mjs} +3 -3
  269. package/dist/{chunk-VSVLIRO2.js → chunk-RXEU77VN.js} +15 -15
  270. package/dist/{chunk-VSVLIRO2.js.map → chunk-RXEU77VN.js.map} +1 -1
  271. package/dist/{chunk-ZRIYCU2V.js → chunk-S2BZPPAI.js} +5 -5
  272. package/dist/{chunk-ZRIYCU2V.js.map → chunk-S2BZPPAI.js.map} +1 -1
  273. package/dist/{chunk-MES2BP4T.mjs → chunk-SGL5IUCV.mjs} +2 -2
  274. package/dist/{chunk-7LS5UQCQ.mjs → chunk-STKUPDHG.mjs} +2 -2
  275. package/dist/{chunk-PPYCCT2K.js → chunk-SWDVSCQJ.js} +9 -8
  276. package/dist/chunk-SWDVSCQJ.js.map +1 -0
  277. package/dist/{chunk-JCA76TCX.mjs → chunk-SZKLJOTN.mjs} +2 -2
  278. package/dist/{chunk-F5WR6XZZ.js → chunk-T73HPYF2.js} +3 -3
  279. package/dist/{chunk-F5WR6XZZ.js.map → chunk-T73HPYF2.js.map} +1 -1
  280. package/dist/{chunk-ZB6HDDE4.js → chunk-TQO37PVW.js} +14 -14
  281. package/dist/{chunk-ZB6HDDE4.js.map → chunk-TQO37PVW.js.map} +1 -1
  282. package/dist/{chunk-VKQ5AJDT.mjs → chunk-UENZQF2Y.mjs} +2 -2
  283. package/dist/{chunk-DCG7EUBC.js → chunk-UWLW5YVB.js} +6 -6
  284. package/dist/{chunk-DCG7EUBC.js.map → chunk-UWLW5YVB.js.map} +1 -1
  285. package/dist/{chunk-37AJ2GVO.mjs → chunk-UXJIZDXQ.mjs} +2 -2
  286. package/dist/{chunk-PLLPFCN3.js → chunk-UXMDPRAI.js} +14 -14
  287. package/dist/{chunk-PLLPFCN3.js.map → chunk-UXMDPRAI.js.map} +1 -1
  288. package/dist/{chunk-7BOTKRY6.mjs → chunk-V55DG5B4.mjs} +2 -2
  289. package/dist/{chunk-YRTIDZTN.js → chunk-VHEDMUAW.js} +4 -4
  290. package/dist/{chunk-YRTIDZTN.js.map → chunk-VHEDMUAW.js.map} +1 -1
  291. package/dist/{chunk-4RD55E4R.js → chunk-VWLWSO4J.js} +3 -3
  292. package/dist/{chunk-4RD55E4R.js.map → chunk-VWLWSO4J.js.map} +1 -1
  293. package/dist/{chunk-VJQIKFMG.mjs → chunk-W3CWEZDB.mjs} +3 -3
  294. package/dist/{chunk-JM6SLU7D.js → chunk-W7NNH4VY.js} +6 -6
  295. package/dist/{chunk-JM6SLU7D.js.map → chunk-W7NNH4VY.js.map} +1 -1
  296. package/dist/{chunk-VQ7MSYIH.js → chunk-WUN5NHZE.js} +4 -4
  297. package/dist/{chunk-VQ7MSYIH.js.map → chunk-WUN5NHZE.js.map} +1 -1
  298. package/dist/{chunk-AAU7ABJN.js → chunk-WWTRSQ3X.js} +3 -3
  299. package/dist/{chunk-AAU7ABJN.js.map → chunk-WWTRSQ3X.js.map} +1 -1
  300. package/dist/{chunk-BR6D2TGF.js → chunk-WZOA2PVJ.js} +3 -3
  301. package/dist/{chunk-BR6D2TGF.js.map → chunk-WZOA2PVJ.js.map} +1 -1
  302. package/dist/{chunk-TZKR5Z4J.mjs → chunk-XJF2F36N.mjs} +2 -2
  303. package/dist/{chunk-P6ACVNQK.mjs → chunk-YD2XBEKO.mjs} +2 -2
  304. package/dist/{chunk-LMXWIPGR.js → chunk-YWPLHFAK.js} +3 -3
  305. package/dist/{chunk-LMXWIPGR.js.map → chunk-YWPLHFAK.js.map} +1 -1
  306. package/dist/{chunk-DCEWWHV5.mjs → chunk-Z3HGYPH7.mjs} +2 -2
  307. package/dist/{chunk-Q3PKW6TH.js → chunk-ZCV3HWI2.js} +6 -6
  308. package/dist/{chunk-Q3PKW6TH.js.map → chunk-ZCV3HWI2.js.map} +1 -1
  309. package/dist/{chunk-OSA3YVDZ.mjs → chunk-ZF46DCAC.mjs} +2 -2
  310. package/dist/{chunk-5T6IDXSB.js → chunk-ZO2GA23I.js} +3 -3
  311. package/dist/{chunk-5T6IDXSB.js.map → chunk-ZO2GA23I.js.map} +1 -1
  312. package/dist/{chunk-ZN3763VZ.js → chunk-ZQLW4EJB.js} +9 -9
  313. package/dist/{chunk-ZN3763VZ.js.map → chunk-ZQLW4EJB.js.map} +1 -1
  314. package/dist/{chunk-CM5R4SPF.js → chunk-ZQQJTO6T.js} +3 -3
  315. package/dist/{chunk-CM5R4SPF.js.map → chunk-ZQQJTO6T.js.map} +1 -1
  316. package/dist/{chunk-SEL4QDCO.js → chunk-ZSXSAE3D.js} +3 -3
  317. package/dist/{chunk-SEL4QDCO.js.map → chunk-ZSXSAE3D.js.map} +1 -1
  318. package/dist/components/SupportChatWidget/guestSupportChatAdapter.d.ts +6 -1
  319. package/dist/components/SupportChatWidget/guestSupportChatAdapter.d.ts.map +1 -1
  320. package/dist/hooks/useAppContent/index.js +49 -49
  321. package/dist/hooks/useAppContent/index.mjs +48 -48
  322. package/dist/hooks/useSupportChat.d.ts +2 -0
  323. package/dist/hooks/useSupportChat.d.ts.map +1 -1
  324. package/dist/index.js +49 -49
  325. package/dist/index.mjs +48 -48
  326. package/dist/types/supportChat/index.d.ts +14 -0
  327. package/dist/types/supportChat/index.d.ts.map +1 -1
  328. package/dist/types/supportChat/index.js +2 -2
  329. package/dist/types/supportChat/index.mjs +1 -1
  330. package/dist/types/supportChat.d.ts +14 -0
  331. package/dist/types/supportChat.d.ts.map +1 -1
  332. package/package.json +1 -1
  333. package/dist/chunk-CYSODZGX.js.map +0 -1
  334. package/dist/chunk-OES54VU6.mjs.map +0 -1
  335. package/dist/chunk-PPYCCT2K.js.map +0 -1
  336. package/dist/chunk-SU5Q64TW.mjs.map +0 -1
  337. /package/dist/{RichEditorCore-6MTYSSKS.mjs.map → RichEditorCore-2S5YUAOI.mjs.map} +0 -0
  338. /package/dist/{chunk-B6QOBHZM.mjs.map → chunk-24BEZK43.mjs.map} +0 -0
  339. /package/dist/{chunk-OQTXTDSK.mjs.map → chunk-2OJ3QSLU.mjs.map} +0 -0
  340. /package/dist/{chunk-4OUBPEVS.mjs.map → chunk-3HKLGNUE.mjs.map} +0 -0
  341. /package/dist/{chunk-IHBWJM4C.mjs.map → chunk-3Q7RJKUP.mjs.map} +0 -0
  342. /package/dist/{chunk-PY6WD6SB.mjs.map → chunk-3WRB4WCD.mjs.map} +0 -0
  343. /package/dist/{chunk-L52NFNFK.mjs.map → chunk-5VC4BK65.mjs.map} +0 -0
  344. /package/dist/{chunk-C65DG2RI.mjs.map → chunk-7FSBWUOM.mjs.map} +0 -0
  345. /package/dist/{chunk-HCQJR5CE.mjs.map → chunk-7ZWEBNFB.mjs.map} +0 -0
  346. /package/dist/{chunk-3RHNJUXU.mjs.map → chunk-ACSEJOW6.mjs.map} +0 -0
  347. /package/dist/{chunk-WQZHUW7C.mjs.map → chunk-ANFHLSDB.mjs.map} +0 -0
  348. /package/dist/{chunk-H6NQOLQY.mjs.map → chunk-ANFLWCVD.mjs.map} +0 -0
  349. /package/dist/{chunk-5WS6IENQ.mjs.map → chunk-AU2HLQDJ.mjs.map} +0 -0
  350. /package/dist/{chunk-P6FX3NT6.mjs.map → chunk-AZGDHAP5.mjs.map} +0 -0
  351. /package/dist/{chunk-NPB2JTYC.mjs.map → chunk-BO3QVQ37.mjs.map} +0 -0
  352. /package/dist/{chunk-2WI4Q2I2.mjs.map → chunk-BPMWRFSI.mjs.map} +0 -0
  353. /package/dist/{chunk-TEW33GLR.mjs.map → chunk-BXN3VO6I.mjs.map} +0 -0
  354. /package/dist/{chunk-HBZHWSMZ.mjs.map → chunk-D46PJSQ7.mjs.map} +0 -0
  355. /package/dist/{chunk-I6EAPPBL.mjs.map → chunk-DZMYIOKZ.mjs.map} +0 -0
  356. /package/dist/{chunk-I4QGQMXI.mjs.map → chunk-EXYXZFNK.mjs.map} +0 -0
  357. /package/dist/{chunk-M4O46VDY.mjs.map → chunk-GX2PKLN2.mjs.map} +0 -0
  358. /package/dist/{chunk-5BUTINIM.mjs.map → chunk-HLVLSWKD.mjs.map} +0 -0
  359. /package/dist/{chunk-MMS2ELDK.mjs.map → chunk-IIQCS3A2.mjs.map} +0 -0
  360. /package/dist/{chunk-NKWLCZUZ.mjs.map → chunk-JGGUMHM6.mjs.map} +0 -0
  361. /package/dist/{chunk-NVL5TKWD.mjs.map → chunk-JYFLOQYT.mjs.map} +0 -0
  362. /package/dist/{chunk-DWGDEUEW.mjs.map → chunk-KG563P7F.mjs.map} +0 -0
  363. /package/dist/{chunk-TCIC5556.mjs.map → chunk-KH4TEDGD.mjs.map} +0 -0
  364. /package/dist/{chunk-E6CGJBID.mjs.map → chunk-LUHKMY56.mjs.map} +0 -0
  365. /package/dist/{chunk-TVLEUNKE.mjs.map → chunk-LZP5VZCO.mjs.map} +0 -0
  366. /package/dist/{chunk-EHNEBXS7.mjs.map → chunk-M6DXZ2FM.mjs.map} +0 -0
  367. /package/dist/{chunk-B4C5DWAP.mjs.map → chunk-MMMJDO75.mjs.map} +0 -0
  368. /package/dist/{chunk-KWXOYHYK.mjs.map → chunk-P7MORXSE.mjs.map} +0 -0
  369. /package/dist/{chunk-YG4IZ5Q3.mjs.map → chunk-Q2OSEROT.mjs.map} +0 -0
  370. /package/dist/{chunk-L5SXG6T6.mjs.map → chunk-QQKOHKO2.mjs.map} +0 -0
  371. /package/dist/{chunk-K45PTE7F.mjs.map → chunk-RBKQXT73.mjs.map} +0 -0
  372. /package/dist/{chunk-BXLAOYAG.mjs.map → chunk-RIUVPTG7.mjs.map} +0 -0
  373. /package/dist/{chunk-QVB54LBS.mjs.map → chunk-RNZGX6MX.mjs.map} +0 -0
  374. /package/dist/{chunk-MES2BP4T.mjs.map → chunk-SGL5IUCV.mjs.map} +0 -0
  375. /package/dist/{chunk-7LS5UQCQ.mjs.map → chunk-STKUPDHG.mjs.map} +0 -0
  376. /package/dist/{chunk-JCA76TCX.mjs.map → chunk-SZKLJOTN.mjs.map} +0 -0
  377. /package/dist/{chunk-VKQ5AJDT.mjs.map → chunk-UENZQF2Y.mjs.map} +0 -0
  378. /package/dist/{chunk-37AJ2GVO.mjs.map → chunk-UXJIZDXQ.mjs.map} +0 -0
  379. /package/dist/{chunk-7BOTKRY6.mjs.map → chunk-V55DG5B4.mjs.map} +0 -0
  380. /package/dist/{chunk-VJQIKFMG.mjs.map → chunk-W3CWEZDB.mjs.map} +0 -0
  381. /package/dist/{chunk-TZKR5Z4J.mjs.map → chunk-XJF2F36N.mjs.map} +0 -0
  382. /package/dist/{chunk-P6ACVNQK.mjs.map → chunk-YD2XBEKO.mjs.map} +0 -0
  383. /package/dist/{chunk-DCEWWHV5.mjs.map → chunk-Z3HGYPH7.mjs.map} +0 -0
  384. /package/dist/{chunk-OSA3YVDZ.mjs.map → chunk-ZF46DCAC.mjs.map} +0 -0
@@ -1 +1 @@
1
- {"version":3,"sources":["/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/chunk-4RHCBMMP.js","../src/components/AnswerKeyModal/AnswerKeyModal.tsx"],"names":[],"mappings":"AAAA;AACE;AACF,sDAA4B;AAC5B;AACE;AACF,sDAA4B;AAC5B;AACA;ACJA,yEAAgC;AAChC,iEAA4B;AAC5B,yEAAgC;AAChC,6DAA0B;AAwItB,+CAAA;AA7HJ,IAAM,QAAA,EAAU,CAAC,GAAA,EAAK,GAAA,EAAK,GAAA,EAAK,GAAA,EAAK,GAAG,CAAA;AAYxC,IAAM,oBAAA,EASF;AAAA,EACF,CAAA,yCAA+B,CAAA,EAAG;AAAA,IAChC,KAAA,EAAO,SAAA;AAAA,IACP,IAAA,EAAM,4BAAA;AAAA,IACN,SAAA,EAAW,kBAAA;AAAA,IACX,WAAA,EAAa;AAAA,EACf,CAAA;AAAA,EACA,CAAA,6CAAiC,CAAA,EAAG;AAAA,IAClC,KAAA,EAAO,WAAA;AAAA,IACP,IAAA,EAAM,oBAAA;AAAA,IACN,SAAA,EAAW,gBAAA;AAAA,IACX,WAAA,EAAa;AAAA,EACf,CAAA;AAAA,EACA,CAAA,qCAA6B,CAAA,EAAG;AAAA,IAC9B,KAAA,EAAO,WAAA;AAAA,IACP,IAAA,EAAM,4BAAA;AAAA,IACN,SAAA,EAAW,eAAA;AAAA,IACX,WAAA,EAAa;AAAA,EACf,CAAA;AAAA,EACA,CAAA,6CAAiC,CAAA,EAAG;AAAA,IAClC,KAAA,EAAO,2BAAA;AAAA,IACP,IAAA,EAAM,gBAAA;AAAA,IACN,SAAA,EAAW,eAAA;AAAA,IACX,WAAA,EAAa;AAAA,EACf;AACF,CAAA;AAWA,IAAM,cAAA,EAAgB,CAAC,MAAA,EAAA,GAA2B;AAChD,EAAA,MAAM,SAAA,kBAAW,MAAA,mBAAO,eAAA,4BAAA,CAAkB,CAAC,CAAA,6BAAG,UAAA;AAC9C,EAAA,GAAA,CAAI,CAAC,SAAA,GAAY,CAAC,MAAA,CAAO,OAAA,EAAS,OAAO,CAAA,CAAA;AACzC,EAAA,OAAO,MAAA,CAAO,OAAA,CAAQ,SAAA,CAAU,CAAC,MAAA,EAAA,GAAW,MAAA,CAAO,GAAA,IAAO,QAAQ,CAAA;AACpE,CAAA;AAgBA,IAAM,uBAAA,EAAyB,CAC7B,QAAA,EACA,WAAA,EACA,WAAA,EAAA,GACW;AACX,EAAA,MAAM,QAAA,EACJ,YAAA,GAAe,EAAA,EAAI,CAAA,OAAA,EAAU,OAAA,CAAQ,WAAW,CAAC,CAAA,EAAA;AAEnB,EAAA;AAEgB,EAAA;AAClD;AASmB;AACjB,EAAA;AACA,EAAA;AAII;AAC+B,EAAA;AAGjC,EAAA;AACe,EAAA;AAEE,EAAA;AAEA,EAAA;AACjB,IAAA;AACA,IAAA;AAC4C,IAAA;AAC9C,EAAA;AASgB,EAAA;AACc,oBAAA;AAI1B,oBAAA;AAAC,MAAA;AAAA,MAAA;AACI,QAAA;AACE,QAAA;AACE,QAAA;AACD,QAAA;AACM,QAAA;AACF,QAAA;AAET,QAAA;AAAA,MAAA;AACH,IAAA;AAEkC,oBAAA;AAEH,MAAA;AAGvB,MAAA;AAEF,MAAA;AAAC,QAAA;AAAA,QAAA;AACI,UAAA;AAEE,UAAA;AACM,UAAA;AAEV,UAAA;AAAA,QAAA;AAJI,QAAA;AAKP,MAAA;AAGN,IAAA;AAEA,oBAAA;AAAC,MAAA;AAAA,MAAA;AACO,QAAA;AAC8B,QAAA;AACxB,QAAA;AAAA,MAAA;AACd,IAAA;AACF,EAAA;AAEJ;AAcG;AAEiC,kBAAA;AAGhB,kBAAA;AAET,IAAA;AAAA,IAAA;AAEC,MAAA;AACkB,MAAA;AAAA,IAAA;AAFN,IAAA;AAKlB,EAAA;AAEJ;AAGa;ADvGqD;AACA;AACA;AACA","file":"/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/chunk-4RHCBMMP.js","sourcesContent":[null,"import Modal from '../Modal/Modal';\nimport Text from '../Text/Text';\nimport { ANSWER_STATUS, type QuestionResult } from '../Quiz/useQuizStore';\nimport { CheckCircleIcon } from '@phosphor-icons/react/dist/csr/CheckCircle';\nimport { XCircleIcon } from '@phosphor-icons/react/dist/csr/XCircle';\nimport { MinusCircleIcon } from '@phosphor-icons/react/dist/csr/MinusCircle';\nimport { ClockIcon } from '@phosphor-icons/react/dist/csr/Clock';\n\ntype Answer = QuestionResult['answers'][number];\n\nexport interface AnswerKeyModalProps {\n /** Respostas do aluno, na ordem da prova. */\n answers: Answer[];\n onClose: () => void;\n}\n\n/** Letras das alternativas, na ordem em que as opções foram cadastradas. */\nconst LETTERS = ['A', 'B', 'C', 'D', 'E'];\n\n/**\n * Como cada estado da correção se apresenta na linha.\n *\n * O `Record` sobre o enum é proposital: um estado novo em `ANSWER_STATUS` vira\n * erro de compilação aqui, em vez de cair silenciosamente num \"incorreta\".\n *\n * Em branco tem tratamento próprio, e não vermelho, porque numa prova de papel\n * ele é o sintoma de uma bolha que a leitura óptica não reconheceu — é\n * justamente o que o aluno abre este modal para descobrir.\n */\nconst STATUS_PRESENTATION: Record<\n ANSWER_STATUS,\n {\n label: string;\n Icon: typeof CheckCircleIcon;\n iconClass: string;\n /** Cor da bolha marcada; vazia quando o estado não julga a resposta. */\n bubbleClass: string;\n }\n> = {\n [ANSWER_STATUS.RESPOSTA_CORRETA]: {\n label: 'correta',\n Icon: CheckCircleIcon,\n iconClass: 'text-success-600',\n bubbleClass: 'bg-success-200 border-success-300 text-text-900',\n },\n [ANSWER_STATUS.RESPOSTA_INCORRETA]: {\n label: 'incorreta',\n Icon: XCircleIcon,\n iconClass: 'text-error-600',\n bubbleClass: 'bg-error-200 border-error-300 text-text-900',\n },\n [ANSWER_STATUS.NAO_RESPONDIDO]: {\n label: 'em branco',\n Icon: MinusCircleIcon,\n iconClass: 'text-text-400',\n bubbleClass: 'bg-background-100 border-border-300 text-text-900',\n },\n [ANSWER_STATUS.PENDENTE_AVALIACAO]: {\n label: 'aguardando correção',\n Icon: ClockIcon,\n iconClass: 'text-info-600',\n bubbleClass: 'bg-info-background border-info-300 text-text-900',\n },\n};\n\n/**\n * Posição da alternativa que o aluno marcou.\n *\n * A letra não é gravada: ela é a posição da opção na questão — a mesma regra\n * que a leitura óptica usa para transformar a bolha preenchida em resposta.\n *\n * @param answer - Resposta do aluno a uma questão\n * @returns O índice marcado, ou -1 quando ficou em branco\n */\nconst selectedIndex = (answer: Answer): number => {\n const selected = answer.selectedOptions?.[0]?.optionId;\n if (!selected || !answer.options) return -1;\n return answer.options.findIndex((option) => option.id === selected);\n};\n\n/**\n * Builds the sentence read by screen readers for one answer key row.\n *\n * @param sequence - Question number in the exam\n * @param markedIndex - Index of the marked alternative, or -1 when blank\n * @param statusLabel - Verdict label (e.g. \"correta\"), or null when blank\n * @returns The spoken summary of the row\n *\n * @example\n * ```typescript\n * getAnswerRowSpokenText(1, 2, 'correta'); // 'Questão 1: marcou C — Correta'\n * getAnswerRowSpokenText(2, -1, null); // 'Questão 2: não respondeu'\n * ```\n */\nconst getAnswerRowSpokenText = (\n sequence: number,\n markedIndex: number,\n statusLabel: string | null\n): string => {\n const marking =\n markedIndex >= 0 ? `marcou ${LETTERS[markedIndex]}` : 'não respondeu';\n const verdict = statusLabel\n ? ` — ${statusLabel.charAt(0).toUpperCase()}${statusLabel.slice(1)}`\n : '';\n return `Questão ${sequence}: ${marking}${verdict}`;\n};\n\n/**\n * Uma linha do gabarito: o número da questão, as cinco bolhas e o veredito.\n *\n * @param answer - Resposta do aluno\n * @param sequence - Número da questão na prova\n * @returns JSX da linha\n */\nconst AnswerRow = ({\n answer,\n sequence,\n}: {\n answer: Answer;\n sequence: number;\n}) => {\n const marked = selectedIndex(answer);\n const presentation =\n STATUS_PRESENTATION[answer.answerStatus] ??\n STATUS_PRESENTATION[ANSWER_STATUS.NAO_RESPONDIDO];\n const { Icon } = presentation;\n const isBlank =\n presentation === STATUS_PRESENTATION[ANSWER_STATUS.NAO_RESPONDIDO];\n // Em branco sem bolha marcada já está dito em \"não respondeu\".\n const spokenText = getAnswerRowSpokenText(\n sequence,\n marked,\n isBlank && marked < 0 ? null : presentation.label\n );\n\n /*\n * A linha é lida como uma frase só (\"Questão 1: marcou C — Correta\"). Número,\n * bolhas e ícone são a versão visual da mesma informação e ficam fora da\n * árvore: antes o leitor lia \"1, A, B, C…\" e um svg com aria-label sem role,\n * que nem todo leitor anuncia.\n */\n return (\n <li className=\"flex items-center gap-3 border border-border-100 rounded-lg px-3 py-2\">\n <Text as=\"span\" className=\"sr-only\">\n {spokenText}\n </Text>\n\n <Text\n as=\"span\"\n size=\"xs\"\n weight=\"semibold\"\n color=\"text-white\"\n aria-hidden=\"true\"\n className=\"w-6 h-6 shrink-0 rounded-full bg-primary-700 flex items-center justify-center\"\n >\n {sequence}\n </Text>\n\n <div aria-hidden=\"true\" className=\"flex items-center gap-1 flex-1\">\n {LETTERS.map((letter, index) => {\n const isMarked = index === marked;\n const tone = isMarked\n ? presentation.bubbleClass\n : 'border-border-200 text-text-600';\n return (\n <Text\n as=\"span\"\n key={letter}\n size=\"xs\"\n className={`w-6 h-6 rounded-full border flex items-center justify-center ${tone}`}\n >\n {letter}\n </Text>\n );\n })}\n </div>\n\n <Icon\n size={18}\n className={`${presentation.iconClass} shrink-0`}\n aria-hidden=\"true\"\n />\n </li>\n );\n};\n\n/**\n * O gabarito como o aluno o preencheu no papel.\n *\n * Mostra a bolha que a leitura óptica reconheceu em cada questão — é assim que\n * ele confere se o que foi lido bate com o que ele marcou, coisa que numa\n * prova impressa não dá para ver de outro jeito.\n *\n * @param answers - Respostas do aluno, na ordem da prova\n * @param onClose - Fecha o modal\n * @returns JSX do modal do gabarito\n */\nconst AnswerKeyModal = ({ answers, onClose }: AnswerKeyModalProps) => (\n <Modal isOpen onClose={onClose} title=\"Gabarito\" size=\"xl\">\n <div className=\"flex flex-col gap-3\">\n <Text size=\"sm\" weight=\"bold\" color=\"text-text-950\">\n Respostas\n </Text>\n <ul className=\"grid gap-3 md:grid-cols-2 lg:grid-cols-3\">\n {answers.map((answer, index) => (\n <AnswerRow\n key={answer.questionId}\n answer={answer}\n sequence={index + 1}\n />\n ))}\n </ul>\n </div>\n </Modal>\n);\n\nexport default AnswerKeyModal;\n"]}
1
+ {"version":3,"sources":["/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/chunk-46VHAL4S.js","../src/components/AnswerKeyModal/AnswerKeyModal.tsx"],"names":[],"mappings":"AAAA;AACE;AACF,sDAA4B;AAC5B;AACE;AACF,sDAA4B;AAC5B;AACA;ACJA,yEAAgC;AAChC,iEAA4B;AAC5B,yEAAgC;AAChC,6DAA0B;AAwItB,+CAAA;AA7HJ,IAAM,QAAA,EAAU,CAAC,GAAA,EAAK,GAAA,EAAK,GAAA,EAAK,GAAA,EAAK,GAAG,CAAA;AAYxC,IAAM,oBAAA,EASF;AAAA,EACF,CAAA,yCAA+B,CAAA,EAAG;AAAA,IAChC,KAAA,EAAO,SAAA;AAAA,IACP,IAAA,EAAM,4BAAA;AAAA,IACN,SAAA,EAAW,kBAAA;AAAA,IACX,WAAA,EAAa;AAAA,EACf,CAAA;AAAA,EACA,CAAA,6CAAiC,CAAA,EAAG;AAAA,IAClC,KAAA,EAAO,WAAA;AAAA,IACP,IAAA,EAAM,oBAAA;AAAA,IACN,SAAA,EAAW,gBAAA;AAAA,IACX,WAAA,EAAa;AAAA,EACf,CAAA;AAAA,EACA,CAAA,qCAA6B,CAAA,EAAG;AAAA,IAC9B,KAAA,EAAO,WAAA;AAAA,IACP,IAAA,EAAM,4BAAA;AAAA,IACN,SAAA,EAAW,eAAA;AAAA,IACX,WAAA,EAAa;AAAA,EACf,CAAA;AAAA,EACA,CAAA,6CAAiC,CAAA,EAAG;AAAA,IAClC,KAAA,EAAO,2BAAA;AAAA,IACP,IAAA,EAAM,gBAAA;AAAA,IACN,SAAA,EAAW,eAAA;AAAA,IACX,WAAA,EAAa;AAAA,EACf;AACF,CAAA;AAWA,IAAM,cAAA,EAAgB,CAAC,MAAA,EAAA,GAA2B;AAChD,EAAA,MAAM,SAAA,kBAAW,MAAA,mBAAO,eAAA,4BAAA,CAAkB,CAAC,CAAA,6BAAG,UAAA;AAC9C,EAAA,GAAA,CAAI,CAAC,SAAA,GAAY,CAAC,MAAA,CAAO,OAAA,EAAS,OAAO,CAAA,CAAA;AACzC,EAAA,OAAO,MAAA,CAAO,OAAA,CAAQ,SAAA,CAAU,CAAC,MAAA,EAAA,GAAW,MAAA,CAAO,GAAA,IAAO,QAAQ,CAAA;AACpE,CAAA;AAgBA,IAAM,uBAAA,EAAyB,CAC7B,QAAA,EACA,WAAA,EACA,WAAA,EAAA,GACW;AACX,EAAA,MAAM,QAAA,EACJ,YAAA,GAAe,EAAA,EAAI,CAAA,OAAA,EAAU,OAAA,CAAQ,WAAW,CAAC,CAAA,EAAA;AAEnB,EAAA;AAEgB,EAAA;AAClD;AASmB;AACjB,EAAA;AACA,EAAA;AAII;AAC+B,EAAA;AAGjC,EAAA;AACe,EAAA;AAEE,EAAA;AAEA,EAAA;AACjB,IAAA;AACA,IAAA;AAC4C,IAAA;AAC9C,EAAA;AASgB,EAAA;AACc,oBAAA;AAI1B,oBAAA;AAAC,MAAA;AAAA,MAAA;AACI,QAAA;AACE,QAAA;AACE,QAAA;AACD,QAAA;AACM,QAAA;AACF,QAAA;AAET,QAAA;AAAA,MAAA;AACH,IAAA;AAEkC,oBAAA;AAEH,MAAA;AAGvB,MAAA;AAEF,MAAA;AAAC,QAAA;AAAA,QAAA;AACI,UAAA;AAEE,UAAA;AACM,UAAA;AAEV,UAAA;AAAA,QAAA;AAJI,QAAA;AAKP,MAAA;AAGN,IAAA;AAEA,oBAAA;AAAC,MAAA;AAAA,MAAA;AACO,QAAA;AAC8B,QAAA;AACxB,QAAA;AAAA,MAAA;AACd,IAAA;AACF,EAAA;AAEJ;AAcG;AAEiC,kBAAA;AAGhB,kBAAA;AAET,IAAA;AAAA,IAAA;AAEC,MAAA;AACkB,MAAA;AAAA,IAAA;AAFN,IAAA;AAKlB,EAAA;AAEJ;AAGa;ADvGqD;AACA;AACA;AACA","file":"/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/chunk-46VHAL4S.js","sourcesContent":[null,"import Modal from '../Modal/Modal';\nimport Text from '../Text/Text';\nimport { ANSWER_STATUS, type QuestionResult } from '../Quiz/useQuizStore';\nimport { CheckCircleIcon } from '@phosphor-icons/react/dist/csr/CheckCircle';\nimport { XCircleIcon } from '@phosphor-icons/react/dist/csr/XCircle';\nimport { MinusCircleIcon } from '@phosphor-icons/react/dist/csr/MinusCircle';\nimport { ClockIcon } from '@phosphor-icons/react/dist/csr/Clock';\n\ntype Answer = QuestionResult['answers'][number];\n\nexport interface AnswerKeyModalProps {\n /** Respostas do aluno, na ordem da prova. */\n answers: Answer[];\n onClose: () => void;\n}\n\n/** Letras das alternativas, na ordem em que as opções foram cadastradas. */\nconst LETTERS = ['A', 'B', 'C', 'D', 'E'];\n\n/**\n * Como cada estado da correção se apresenta na linha.\n *\n * O `Record` sobre o enum é proposital: um estado novo em `ANSWER_STATUS` vira\n * erro de compilação aqui, em vez de cair silenciosamente num \"incorreta\".\n *\n * Em branco tem tratamento próprio, e não vermelho, porque numa prova de papel\n * ele é o sintoma de uma bolha que a leitura óptica não reconheceu — é\n * justamente o que o aluno abre este modal para descobrir.\n */\nconst STATUS_PRESENTATION: Record<\n ANSWER_STATUS,\n {\n label: string;\n Icon: typeof CheckCircleIcon;\n iconClass: string;\n /** Cor da bolha marcada; vazia quando o estado não julga a resposta. */\n bubbleClass: string;\n }\n> = {\n [ANSWER_STATUS.RESPOSTA_CORRETA]: {\n label: 'correta',\n Icon: CheckCircleIcon,\n iconClass: 'text-success-600',\n bubbleClass: 'bg-success-200 border-success-300 text-text-900',\n },\n [ANSWER_STATUS.RESPOSTA_INCORRETA]: {\n label: 'incorreta',\n Icon: XCircleIcon,\n iconClass: 'text-error-600',\n bubbleClass: 'bg-error-200 border-error-300 text-text-900',\n },\n [ANSWER_STATUS.NAO_RESPONDIDO]: {\n label: 'em branco',\n Icon: MinusCircleIcon,\n iconClass: 'text-text-400',\n bubbleClass: 'bg-background-100 border-border-300 text-text-900',\n },\n [ANSWER_STATUS.PENDENTE_AVALIACAO]: {\n label: 'aguardando correção',\n Icon: ClockIcon,\n iconClass: 'text-info-600',\n bubbleClass: 'bg-info-background border-info-300 text-text-900',\n },\n};\n\n/**\n * Posição da alternativa que o aluno marcou.\n *\n * A letra não é gravada: ela é a posição da opção na questão — a mesma regra\n * que a leitura óptica usa para transformar a bolha preenchida em resposta.\n *\n * @param answer - Resposta do aluno a uma questão\n * @returns O índice marcado, ou -1 quando ficou em branco\n */\nconst selectedIndex = (answer: Answer): number => {\n const selected = answer.selectedOptions?.[0]?.optionId;\n if (!selected || !answer.options) return -1;\n return answer.options.findIndex((option) => option.id === selected);\n};\n\n/**\n * Builds the sentence read by screen readers for one answer key row.\n *\n * @param sequence - Question number in the exam\n * @param markedIndex - Index of the marked alternative, or -1 when blank\n * @param statusLabel - Verdict label (e.g. \"correta\"), or null when blank\n * @returns The spoken summary of the row\n *\n * @example\n * ```typescript\n * getAnswerRowSpokenText(1, 2, 'correta'); // 'Questão 1: marcou C — Correta'\n * getAnswerRowSpokenText(2, -1, null); // 'Questão 2: não respondeu'\n * ```\n */\nconst getAnswerRowSpokenText = (\n sequence: number,\n markedIndex: number,\n statusLabel: string | null\n): string => {\n const marking =\n markedIndex >= 0 ? `marcou ${LETTERS[markedIndex]}` : 'não respondeu';\n const verdict = statusLabel\n ? ` — ${statusLabel.charAt(0).toUpperCase()}${statusLabel.slice(1)}`\n : '';\n return `Questão ${sequence}: ${marking}${verdict}`;\n};\n\n/**\n * Uma linha do gabarito: o número da questão, as cinco bolhas e o veredito.\n *\n * @param answer - Resposta do aluno\n * @param sequence - Número da questão na prova\n * @returns JSX da linha\n */\nconst AnswerRow = ({\n answer,\n sequence,\n}: {\n answer: Answer;\n sequence: number;\n}) => {\n const marked = selectedIndex(answer);\n const presentation =\n STATUS_PRESENTATION[answer.answerStatus] ??\n STATUS_PRESENTATION[ANSWER_STATUS.NAO_RESPONDIDO];\n const { Icon } = presentation;\n const isBlank =\n presentation === STATUS_PRESENTATION[ANSWER_STATUS.NAO_RESPONDIDO];\n // Em branco sem bolha marcada já está dito em \"não respondeu\".\n const spokenText = getAnswerRowSpokenText(\n sequence,\n marked,\n isBlank && marked < 0 ? null : presentation.label\n );\n\n /*\n * A linha é lida como uma frase só (\"Questão 1: marcou C — Correta\"). Número,\n * bolhas e ícone são a versão visual da mesma informação e ficam fora da\n * árvore: antes o leitor lia \"1, A, B, C…\" e um svg com aria-label sem role,\n * que nem todo leitor anuncia.\n */\n return (\n <li className=\"flex items-center gap-3 border border-border-100 rounded-lg px-3 py-2\">\n <Text as=\"span\" className=\"sr-only\">\n {spokenText}\n </Text>\n\n <Text\n as=\"span\"\n size=\"xs\"\n weight=\"semibold\"\n color=\"text-white\"\n aria-hidden=\"true\"\n className=\"w-6 h-6 shrink-0 rounded-full bg-primary-700 flex items-center justify-center\"\n >\n {sequence}\n </Text>\n\n <div aria-hidden=\"true\" className=\"flex items-center gap-1 flex-1\">\n {LETTERS.map((letter, index) => {\n const isMarked = index === marked;\n const tone = isMarked\n ? presentation.bubbleClass\n : 'border-border-200 text-text-600';\n return (\n <Text\n as=\"span\"\n key={letter}\n size=\"xs\"\n className={`w-6 h-6 rounded-full border flex items-center justify-center ${tone}`}\n >\n {letter}\n </Text>\n );\n })}\n </div>\n\n <Icon\n size={18}\n className={`${presentation.iconClass} shrink-0`}\n aria-hidden=\"true\"\n />\n </li>\n );\n};\n\n/**\n * O gabarito como o aluno o preencheu no papel.\n *\n * Mostra a bolha que a leitura óptica reconheceu em cada questão — é assim que\n * ele confere se o que foi lido bate com o que ele marcou, coisa que numa\n * prova impressa não dá para ver de outro jeito.\n *\n * @param answers - Respostas do aluno, na ordem da prova\n * @param onClose - Fecha o modal\n * @returns JSX do modal do gabarito\n */\nconst AnswerKeyModal = ({ answers, onClose }: AnswerKeyModalProps) => (\n <Modal isOpen onClose={onClose} title=\"Gabarito\" size=\"xl\">\n <div className=\"flex flex-col gap-3\">\n <Text size=\"sm\" weight=\"bold\" color=\"text-text-950\">\n Respostas\n </Text>\n <ul className=\"grid gap-3 md:grid-cols-2 lg:grid-cols-3\">\n {answers.map((answer, index) => (\n <AnswerRow\n key={answer.questionId}\n answer={answer}\n sequence={index + 1}\n />\n ))}\n </ul>\n </div>\n </Modal>\n);\n\nexport default AnswerKeyModal;\n"]}
@@ -73,4 +73,4 @@ var SUPPORT_CHAT_ATTACHMENT_RULES = {
73
73
 
74
74
 
75
75
  exports.SUPPORT_CHAT_MESSAGE_TYPE = SUPPORT_CHAT_MESSAGE_TYPE; exports.SUPPORT_CHAT_SEND_STATUS = SUPPORT_CHAT_SEND_STATUS; exports.SUPPORT_CHAT_REPLY_MODE = SUPPORT_CHAT_REPLY_MODE; exports.SUPPORT_CHAT_EVENT_TYPE = SUPPORT_CHAT_EVENT_TYPE; exports.SUPPORT_CHAT_STATUS = SUPPORT_CHAT_STATUS; exports.SUPPORT_CHAT_HANDLER = SUPPORT_CHAT_HANDLER; exports.SUPPORT_CHAT_AUTHOR = SUPPORT_CHAT_AUTHOR; exports.SUPPORT_CHAT_GUEST_SENDER_ID = SUPPORT_CHAT_GUEST_SENDER_ID; exports.SUPPORT_CHAT_IMAGE_TYPES = SUPPORT_CHAT_IMAGE_TYPES; exports.SUPPORT_CHAT_MAX_IMAGE_BYTES = SUPPORT_CHAT_MAX_IMAGE_BYTES; exports.SUPPORT_CHAT_MAX_ATTACHMENTS = SUPPORT_CHAT_MAX_ATTACHMENTS; exports.SUPPORT_CHAT_ATTACHMENT_RULES = SUPPORT_CHAT_ATTACHMENT_RULES;
76
- //# sourceMappingURL=chunk-POT3ALY3.js.map
76
+ //# sourceMappingURL=chunk-5DINXU6W.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/chunk-POT3ALY3.js","../src/types/supportChat.ts"],"names":[],"mappings":"AAAA;ACWO,IAAM,0BAAA,EAA4B;AAAA,EACvC,IAAA,EAAM,MAAA;AAAA,EACN,KAAA,EAAO,OAAA;AAAA,EACP,EAAA,EAAI,IAAA;AAAA,EACJ,KAAA,EAAO,OAAA;AAAA,EACP,aAAA,EAAe;AACjB,CAAA;AAQO,IAAM,yBAAA,EAA2B;AAAA,EACtC,IAAA,EAAM,MAAA;AAAA,EACN,OAAA,EAAS,SAAA;AAAA,EACT,MAAA,EAAQ;AACV,CAAA;AAsBO,IAAM,wBAAA,EAA0B;AAAA,EACrC,IAAA,EAAM,MAAA;AAAA,EACN,aAAA,EAAe;AACjB,CAAA;AAQO,IAAM,wBAAA,EAA0B;AAAA,EACrC,OAAA,EAAS,SAAA;AAAA,EACT,QAAA,EAAU,UAAA;AAAA,EACV,EAAA,EAAI,IAAA;AAAA,EACJ,OAAA,EAAS,SAAA;AAAA,EACT,IAAA,EAAM;AACR,CAAA;AA+BO,IAAM,oBAAA,EAAsB;AAAA,EACjC,MAAA,EAAQ,QAAA;AAAA,EACR,QAAA,EAAU,UAAA;AAAA,EACV,YAAA,EAAc,cAAA;AAAA,EACd,OAAA,EAAS;AACX,CAAA;AAQO,IAAM,qBAAA,EAAuB;AAAA,EAClC,EAAA,EAAI,IAAA;AAAA,EACJ,MAAA,EAAQ;AACV,CAAA;AAQO,IAAM,oBAAA,EAAsB;AAAA,EACjC,IAAA,EAAM,MAAA;AAAA,EACN,KAAA,EAAO,OAAA;AAAA,EACP,KAAA,EAAO,OAAA;AAAA,EACP,EAAA,EAAI;AACN,CAAA;AAMO,IAAM,6BAAA,EAA+B,OAAA;AAGrC,IAAM,yBAAA,EAA2B;AAAA,EACtC,YAAA;AAAA,EACA,WAAA;AAAA,EACA,WAAA;AAAA,EACA;AACF,CAAA;AAGO,IAAM,6BAAA,EAA+B,EAAA,EAAI,KAAA,EAAO,IAAA;AAGhD,IAAM,6BAAA,EAA+B,CAAA;AAE5C,IAAM,GAAA,EAAK,KAAA,EAAO,IAAA;AAMX,IAAM,8BAAA,EAAwD;AAAA,EACnE,YAAA,EAAc,EAAA,EAAI,EAAA;AAAA,EAClB,WAAA,EAAa,EAAA,EAAI,EAAA;AAAA,EACjB,WAAA,EAAa,EAAA,EAAI,EAAA;AAAA,EACjB,YAAA,EAAc,EAAA,EAAI,EAAA;AAAA,EAClB,iBAAA,EAAmB,GAAA,EAAK,EAAA;AAAA,EACxB,WAAA,EAAa,GAAA,EAAK,EAAA;AAAA,EAClB,YAAA,EAAc,GAAA,EAAK,EAAA;AAAA,EACnB,iBAAA,EAAmB,GAAA,EAAK;AAC1B,CAAA;ADzGA;AACA;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,+tBAAC","file":"/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/chunk-POT3ALY3.js","sourcesContent":[null,"import type { SupportType } from './support';\n\n/**\n * Support (SAC) chat types\n */\n\n/**\n * Who authored a support chat message. `USER`, `GUEST`, `AI` and `AGENT`\n * match the backend `senderRole`; `GUEST` (logged-out visitor) renders like `USER`.\n * `INTERNAL_NOTE` is only visible in the backoffice.\n */\nexport const SUPPORT_CHAT_MESSAGE_TYPE = {\n USER: 'USER',\n GUEST: 'GUEST',\n AI: 'AI',\n AGENT: 'AGENT',\n INTERNAL_NOTE: 'INTERNAL_NOTE',\n} as const;\n\nexport type SupportChatMessageType =\n (typeof SUPPORT_CHAT_MESSAGE_TYPE)[keyof typeof SUPPORT_CHAT_MESSAGE_TYPE];\n\n/**\n * Delivery status of a message sent by the current user/agent\n */\nexport const SUPPORT_CHAT_SEND_STATUS = {\n SENT: 'SENT',\n SENDING: 'SENDING',\n FAILED: 'FAILED',\n} as const;\n\nexport type SupportChatSendStatus =\n (typeof SUPPORT_CHAT_SEND_STATUS)[keyof typeof SUPPORT_CHAT_SEND_STATUS];\n\n/**\n * File attached to a support chat message (same shape as the backend attachment)\n */\nexport interface SupportChatAttachment {\n id: string;\n fileName: string;\n url: string;\n /** MIME type; `image/*` renders a thumbnail */\n mimeType?: string;\n /** File size in bytes */\n size?: number;\n}\n\n/**\n * Where an agent reply goes: to the requester (`USER`) or only to the team\n * (`INTERNAL_NOTE`, same value as the message type shown in the backoffice)\n */\nexport const SUPPORT_CHAT_REPLY_MODE = {\n USER: 'USER',\n INTERNAL_NOTE: 'INTERNAL_NOTE',\n} as const;\n\nexport type SupportChatReplyMode =\n (typeof SUPPORT_CHAT_REPLY_MODE)[keyof typeof SUPPORT_CHAT_REPLY_MODE];\n\n/**\n * Kind of event shown between chat messages\n */\nexport const SUPPORT_CHAT_EVENT_TYPE = {\n NEUTRAL: 'NEUTRAL',\n TRANSFER: 'TRANSFER',\n AI: 'AI',\n SUCCESS: 'SUCCESS',\n DATE: 'DATE',\n} as const;\n\nexport type SupportChatEventType =\n (typeof SUPPORT_CHAT_EVENT_TYPE)[keyof typeof SUPPORT_CHAT_EVENT_TYPE];\n\n/**\n * Author displayed next to a support chat message\n */\nexport interface SupportChatAuthor {\n name: string;\n /** Overrides the initials computed from `name` */\n initials?: string;\n /** Tailwind classes overriding the avatar background color */\n avatarClassName?: string;\n}\n\n/**\n * FAQ entry the AI answer was based on\n */\nexport interface SupportChatFaqReference {\n title: string;\n onClick?: () => void;\n}\n\n/*\n * Support chat widget (floating chat on top of a SupportChatAdapter)\n */\n\n/**\n * Ticket status, as sent by the backend (sup_support.status)\n */\nexport const SUPPORT_CHAT_STATUS = {\n ABERTO: 'ABERTO',\n PENDENTE: 'PENDENTE',\n EM_ANDAMENTO: 'EM_ANDAMENTO',\n FECHADO: 'FECHADO',\n} as const;\n\nexport type SupportChatStatus =\n (typeof SUPPORT_CHAT_STATUS)[keyof typeof SUPPORT_CHAT_STATUS];\n\n/**\n * Who is handling the ticket\n */\nexport const SUPPORT_CHAT_HANDLER = {\n IA: 'IA',\n HUMANO: 'HUMANO',\n} as const;\n\nexport type SupportChatHandler =\n (typeof SUPPORT_CHAT_HANDLER)[keyof typeof SUPPORT_CHAT_HANDLER];\n\n/**\n * Author of a message (sent by the backend as senderRole)\n */\nexport const SUPPORT_CHAT_AUTHOR = {\n USER: 'USER',\n GUEST: 'GUEST',\n AGENT: 'AGENT',\n AI: 'AI',\n} as const;\n\nexport type SupportChatAuthorRole =\n (typeof SUPPORT_CHAT_AUTHOR)[keyof typeof SUPPORT_CHAT_AUTHOR];\n\n/** senderId of the messages written by a visitor (not logged in) */\nexport const SUPPORT_CHAT_GUEST_SENDER_ID = 'guest';\n\n/** Images accepted by the backend */\nexport const SUPPORT_CHAT_IMAGE_TYPES = [\n 'image/jpeg',\n 'image/png',\n 'image/gif',\n 'image/webp',\n];\n\n/** Largest image the backend accepts (5 MB) */\nexport const SUPPORT_CHAT_MAX_IMAGE_BYTES = 5 * 1024 * 1024;\n\n/** Most attachments in one message */\nexport const SUPPORT_CHAT_MAX_ATTACHMENTS = 5;\n\nconst MB = 1024 * 1024;\n\n/**\n * Files the backend accepts in the support chat (mime type → largest size).\n * Mirrors SUPPORT_ATTACHMENT_RULES of the API: images, PDF and short videos.\n */\nexport const SUPPORT_CHAT_ATTACHMENT_RULES: Record<string, number> = {\n 'image/jpeg': 5 * MB,\n 'image/png': 5 * MB,\n 'image/gif': 5 * MB,\n 'image/webp': 5 * MB,\n 'application/pdf': 10 * MB,\n 'video/mp4': 50 * MB,\n 'video/webm': 50 * MB,\n 'video/quicktime': 50 * MB,\n};\n\n/**\n * FAQ article opened inside the chat (GET /help-center/:institutionId/articles/:id)\n */\nexport interface SupportChatArticle {\n id: string;\n title: string;\n /** Article HTML as saved by the help-center editor */\n body: string;\n /** ISO date of the last update, when known */\n updatedAt: string | null;\n}\n\n/**\n * Chat message (same shape as the platform chat, plus attachments)\n */\nexport interface SupportChatMessage {\n id: string;\n senderId: string;\n senderName: string;\n senderPhoto: string | null;\n senderRole: SupportChatAuthorRole;\n content: string;\n messageType: string;\n createdAt: string;\n attachments: SupportChatAttachment[];\n /** FAQ article an AI message is based on */\n faq?: { id: string; title: string } | null;\n /**\n * Contact fields an AI message asks for: the chat shows a form for them\n * under the message\n */\n contactRequest?: SupportChatContactField[] | null;\n /** Local only: shown before the server confirms it (optimistic) */\n pending?: boolean;\n}\n\n/**\n * Ticket the conversation belongs to\n */\nexport interface SupportChatTicket {\n id: string;\n status: SupportChatStatus;\n handlerType: SupportChatHandler | null;\n problemType: string | null;\n}\n\n/**\n * Realtime events pushed to the conversation (WebSocket in the real adapter)\n */\nexport type SupportChatEvent =\n | { type: 'new_message'; payload: { message: SupportChatMessage } }\n | {\n type: 'message_updated';\n payload: { messageId: string; content: string; read: boolean };\n }\n | {\n type: 'ticket_updated';\n payload: { ticketId: string } & Omit<SupportChatTicket, 'id'>;\n }\n | { type: 'error'; payload: { message: string } };\n\n/**\n * Data needed to open a conversation (first message creates the ticket)\n */\nexport interface StartSupportChatInput {\n message: string;\n channel: string;\n institutionId?: string;\n requesterName?: string;\n email?: string;\n /** Files sent with the first message */\n files?: File[];\n}\n\n/**\n * Data layer of the widget. The mock adapter implements it in memory; the\n * real one will use the support REST API and the chat WebSocket.\n */\nexport interface SupportChatAdapter {\n /**\n * Create the ticket with its first message. With files the API may store\n * them as a second message, hence the list.\n */\n startConversation(input: StartSupportChatInput): Promise<{\n ticket: SupportChatTicket;\n messages: SupportChatMessage[];\n }>;\n /** Send a message (text and/or files) on an existing ticket */\n sendMessage(\n ticketId: string,\n content: string,\n files?: File[]\n ): Promise<SupportChatMessage>;\n /** Messages of the ticket, oldest first */\n listMessages(ticketId: string): Promise<SupportChatMessage[]>;\n /** Listen to realtime events of the ticket; returns the unsubscribe */\n subscribe(\n ticketId: string,\n listener: (event: SupportChatEvent) => void\n ): () => void;\n /**\n * Save the name and/or e-mail the visitor typed in the contact form.\n * Without it the chat does not show the form (the AI request stays a text).\n */\n updateContact?(ticketId: string, contact: SupportChatContact): Promise<void>;\n /**\n * Load a FAQ article of the institution. With it, the article an AI answer\n * is based on opens inside the chat instead of a new tab.\n */\n getArticle?(\n institutionId: string,\n articleId: string\n ): Promise<SupportChatArticle>;\n}\n\n/** Contact field the AI asks for */\nexport type SupportChatContactField = 'name' | 'email';\n\n/** Contact sent by the chat form */\nexport interface SupportChatContact {\n requesterName?: string;\n email?: string;\n}\n\n/**\n * Feature flags of the widget\n */\nexport interface SupportChatFlags {\n /** Widget available at all (default true) */\n enabled?: boolean;\n /** Chat with the support (default true) */\n chat?: boolean;\n /** FAQ (reserved, not rendered yet) */\n faq?: boolean;\n /** AI first answer (reserved, not rendered yet) */\n ai?: boolean;\n /** ZENDESK hides the widget (the Zendesk messenger is used instead) */\n supportType?: SupportType;\n}\n\n/**\n * Visual customization of the widget\n */\nexport interface SupportChatTheme {\n /** Background of the button and the header (any CSS color) */\n primaryColor?: string;\n /** Corner of the screen (default bottom-right) */\n position?: 'bottom-right' | 'bottom-left';\n /** Header title (default \"Suporte\") */\n title?: string;\n}\n\n/**\n * Logged user (optional; a visitor has none)\n */\nexport interface SupportChatUser {\n name?: string;\n email?: string;\n}\n\n/**\n * HTTP client the API adapter needs (axios instances fit as they are)\n */\nexport interface SupportChatHttpClient {\n get: <T>(\n url: string,\n config?: { headers?: Record<string, string> }\n ) => Promise<{ data: T }>;\n post: <T>(\n url: string,\n data?: unknown,\n config?: { headers?: Record<string, string> }\n ) => Promise<{ data: T }>;\n /** Used to save the visitor contact (PUT /support/guest/:id) */\n put?: <T>(\n url: string,\n data?: unknown,\n config?: { headers?: Record<string, string> }\n ) => Promise<{ data: T }>;\n}\n\n/**\n * Message as returned by the support REST routes\n */\nexport interface SupportChatAnswerApi {\n id: string;\n authorType: SupportChatAuthorRole;\n answer: string;\n createdAt: string;\n attachments?: SupportChatAttachment[];\n /** FAQ article an AI message is based on */\n faqArticleId?: string | null;\n faqArticleTitle?: string | null;\n /** Contact fields an AI message asks for */\n contactRequest?: SupportChatContactField[] | null;\n}\n"]}
1
+ {"version":3,"sources":["/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/chunk-5DINXU6W.js","../src/types/supportChat.ts"],"names":[],"mappings":"AAAA;ACWO,IAAM,0BAAA,EAA4B;AAAA,EACvC,IAAA,EAAM,MAAA;AAAA,EACN,KAAA,EAAO,OAAA;AAAA,EACP,EAAA,EAAI,IAAA;AAAA,EACJ,KAAA,EAAO,OAAA;AAAA,EACP,aAAA,EAAe;AACjB,CAAA;AAQO,IAAM,yBAAA,EAA2B;AAAA,EACtC,IAAA,EAAM,MAAA;AAAA,EACN,OAAA,EAAS,SAAA;AAAA,EACT,MAAA,EAAQ;AACV,CAAA;AAsBO,IAAM,wBAAA,EAA0B;AAAA,EACrC,IAAA,EAAM,MAAA;AAAA,EACN,aAAA,EAAe;AACjB,CAAA;AAQO,IAAM,wBAAA,EAA0B;AAAA,EACrC,OAAA,EAAS,SAAA;AAAA,EACT,QAAA,EAAU,UAAA;AAAA,EACV,EAAA,EAAI,IAAA;AAAA,EACJ,OAAA,EAAS,SAAA;AAAA,EACT,IAAA,EAAM;AACR,CAAA;AA+BO,IAAM,oBAAA,EAAsB;AAAA,EACjC,MAAA,EAAQ,QAAA;AAAA,EACR,QAAA,EAAU,UAAA;AAAA,EACV,YAAA,EAAc,cAAA;AAAA,EACd,OAAA,EAAS;AACX,CAAA;AAQO,IAAM,qBAAA,EAAuB;AAAA,EAClC,EAAA,EAAI,IAAA;AAAA,EACJ,MAAA,EAAQ;AACV,CAAA;AAQO,IAAM,oBAAA,EAAsB;AAAA,EACjC,IAAA,EAAM,MAAA;AAAA,EACN,KAAA,EAAO,OAAA;AAAA,EACP,KAAA,EAAO,OAAA;AAAA,EACP,EAAA,EAAI;AACN,CAAA;AAMO,IAAM,6BAAA,EAA+B,OAAA;AAGrC,IAAM,yBAAA,EAA2B;AAAA,EACtC,YAAA;AAAA,EACA,WAAA;AAAA,EACA,WAAA;AAAA,EACA;AACF,CAAA;AAGO,IAAM,6BAAA,EAA+B,EAAA,EAAI,KAAA,EAAO,IAAA;AAGhD,IAAM,6BAAA,EAA+B,CAAA;AAE5C,IAAM,GAAA,EAAK,KAAA,EAAO,IAAA;AAMX,IAAM,8BAAA,EAAwD;AAAA,EACnE,YAAA,EAAc,EAAA,EAAI,EAAA;AAAA,EAClB,WAAA,EAAa,EAAA,EAAI,EAAA;AAAA,EACjB,WAAA,EAAa,EAAA,EAAI,EAAA;AAAA,EACjB,YAAA,EAAc,EAAA,EAAI,EAAA;AAAA,EAClB,iBAAA,EAAmB,GAAA,EAAK,EAAA;AAAA,EACxB,WAAA,EAAa,GAAA,EAAK,EAAA;AAAA,EAClB,YAAA,EAAc,GAAA,EAAK,EAAA;AAAA,EACnB,iBAAA,EAAmB,GAAA,EAAK;AAC1B,CAAA;ADzGA;AACA;AACE;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACF,+tBAAC","file":"/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/chunk-5DINXU6W.js","sourcesContent":[null,"import type { SupportType } from './support';\n\n/**\n * Support (SAC) chat types\n */\n\n/**\n * Who authored a support chat message. `USER`, `GUEST`, `AI` and `AGENT`\n * match the backend `senderRole`; `GUEST` (logged-out visitor) renders like `USER`.\n * `INTERNAL_NOTE` is only visible in the backoffice.\n */\nexport const SUPPORT_CHAT_MESSAGE_TYPE = {\n USER: 'USER',\n GUEST: 'GUEST',\n AI: 'AI',\n AGENT: 'AGENT',\n INTERNAL_NOTE: 'INTERNAL_NOTE',\n} as const;\n\nexport type SupportChatMessageType =\n (typeof SUPPORT_CHAT_MESSAGE_TYPE)[keyof typeof SUPPORT_CHAT_MESSAGE_TYPE];\n\n/**\n * Delivery status of a message sent by the current user/agent\n */\nexport const SUPPORT_CHAT_SEND_STATUS = {\n SENT: 'SENT',\n SENDING: 'SENDING',\n FAILED: 'FAILED',\n} as const;\n\nexport type SupportChatSendStatus =\n (typeof SUPPORT_CHAT_SEND_STATUS)[keyof typeof SUPPORT_CHAT_SEND_STATUS];\n\n/**\n * File attached to a support chat message (same shape as the backend attachment)\n */\nexport interface SupportChatAttachment {\n id: string;\n fileName: string;\n url: string;\n /** MIME type; `image/*` renders a thumbnail */\n mimeType?: string;\n /** File size in bytes */\n size?: number;\n}\n\n/**\n * Where an agent reply goes: to the requester (`USER`) or only to the team\n * (`INTERNAL_NOTE`, same value as the message type shown in the backoffice)\n */\nexport const SUPPORT_CHAT_REPLY_MODE = {\n USER: 'USER',\n INTERNAL_NOTE: 'INTERNAL_NOTE',\n} as const;\n\nexport type SupportChatReplyMode =\n (typeof SUPPORT_CHAT_REPLY_MODE)[keyof typeof SUPPORT_CHAT_REPLY_MODE];\n\n/**\n * Kind of event shown between chat messages\n */\nexport const SUPPORT_CHAT_EVENT_TYPE = {\n NEUTRAL: 'NEUTRAL',\n TRANSFER: 'TRANSFER',\n AI: 'AI',\n SUCCESS: 'SUCCESS',\n DATE: 'DATE',\n} as const;\n\nexport type SupportChatEventType =\n (typeof SUPPORT_CHAT_EVENT_TYPE)[keyof typeof SUPPORT_CHAT_EVENT_TYPE];\n\n/**\n * Author displayed next to a support chat message\n */\nexport interface SupportChatAuthor {\n name: string;\n /** Overrides the initials computed from `name` */\n initials?: string;\n /** Tailwind classes overriding the avatar background color */\n avatarClassName?: string;\n}\n\n/**\n * FAQ entry the AI answer was based on\n */\nexport interface SupportChatFaqReference {\n title: string;\n onClick?: () => void;\n}\n\n/*\n * Support chat widget (floating chat on top of a SupportChatAdapter)\n */\n\n/**\n * Ticket status, as sent by the backend (sup_support.status)\n */\nexport const SUPPORT_CHAT_STATUS = {\n ABERTO: 'ABERTO',\n PENDENTE: 'PENDENTE',\n EM_ANDAMENTO: 'EM_ANDAMENTO',\n FECHADO: 'FECHADO',\n} as const;\n\nexport type SupportChatStatus =\n (typeof SUPPORT_CHAT_STATUS)[keyof typeof SUPPORT_CHAT_STATUS];\n\n/**\n * Who is handling the ticket\n */\nexport const SUPPORT_CHAT_HANDLER = {\n IA: 'IA',\n HUMANO: 'HUMANO',\n} as const;\n\nexport type SupportChatHandler =\n (typeof SUPPORT_CHAT_HANDLER)[keyof typeof SUPPORT_CHAT_HANDLER];\n\n/**\n * Author of a message (sent by the backend as senderRole)\n */\nexport const SUPPORT_CHAT_AUTHOR = {\n USER: 'USER',\n GUEST: 'GUEST',\n AGENT: 'AGENT',\n AI: 'AI',\n} as const;\n\nexport type SupportChatAuthorRole =\n (typeof SUPPORT_CHAT_AUTHOR)[keyof typeof SUPPORT_CHAT_AUTHOR];\n\n/** senderId of the messages written by a visitor (not logged in) */\nexport const SUPPORT_CHAT_GUEST_SENDER_ID = 'guest';\n\n/** Images accepted by the backend */\nexport const SUPPORT_CHAT_IMAGE_TYPES = [\n 'image/jpeg',\n 'image/png',\n 'image/gif',\n 'image/webp',\n];\n\n/** Largest image the backend accepts (5 MB) */\nexport const SUPPORT_CHAT_MAX_IMAGE_BYTES = 5 * 1024 * 1024;\n\n/** Most attachments in one message */\nexport const SUPPORT_CHAT_MAX_ATTACHMENTS = 5;\n\nconst MB = 1024 * 1024;\n\n/**\n * Files the backend accepts in the support chat (mime type → largest size).\n * Mirrors SUPPORT_ATTACHMENT_RULES of the API: images, PDF and short videos.\n */\nexport const SUPPORT_CHAT_ATTACHMENT_RULES: Record<string, number> = {\n 'image/jpeg': 5 * MB,\n 'image/png': 5 * MB,\n 'image/gif': 5 * MB,\n 'image/webp': 5 * MB,\n 'application/pdf': 10 * MB,\n 'video/mp4': 50 * MB,\n 'video/webm': 50 * MB,\n 'video/quicktime': 50 * MB,\n};\n\n/**\n * FAQ article opened inside the chat (GET /help-center/:institutionId/articles/:id)\n */\nexport interface SupportChatArticle {\n id: string;\n title: string;\n /** Article HTML as saved by the help-center editor */\n body: string;\n /** ISO date of the last update, when known */\n updatedAt: string | null;\n}\n\n/**\n * Chat message (same shape as the platform chat, plus attachments)\n */\nexport interface SupportChatMessage {\n id: string;\n senderId: string;\n senderName: string;\n senderPhoto: string | null;\n senderRole: SupportChatAuthorRole;\n content: string;\n messageType: string;\n createdAt: string;\n attachments: SupportChatAttachment[];\n /** FAQ article an AI message is based on */\n faq?: { id: string; title: string } | null;\n /**\n * Contact fields an AI message asks for: the chat shows a form for them\n * under the message\n */\n contactRequest?: SupportChatContactField[] | null;\n /** Local only: shown before the server confirms it (optimistic) */\n pending?: boolean;\n}\n\n/**\n * Ticket the conversation belongs to\n */\nexport interface SupportChatTicket {\n id: string;\n status: SupportChatStatus;\n handlerType: SupportChatHandler | null;\n problemType: string | null;\n}\n\n/**\n * Realtime events pushed to the conversation (WebSocket in the real adapter)\n */\nexport type SupportChatEvent =\n | { type: 'new_message'; payload: { message: SupportChatMessage } }\n | {\n type: 'message_updated';\n payload: { messageId: string; content: string; read: boolean };\n }\n | {\n type: 'ticket_updated';\n payload: { ticketId: string } & Omit<SupportChatTicket, 'id'>;\n }\n | { type: 'error'; payload: { message: string } };\n\n/**\n * Data needed to open a conversation (first message creates the ticket)\n */\nexport interface StartSupportChatInput {\n message: string;\n channel: string;\n institutionId?: string;\n requesterName?: string;\n email?: string;\n /** Files sent with the first message */\n files?: File[];\n}\n\n/**\n * Data layer of the widget. The mock adapter implements it in memory; the\n * real one will use the support REST API and the chat WebSocket.\n */\nexport interface SupportChatAdapter {\n /**\n * Create the ticket with its first message. With files the API may store\n * them as a second message, hence the list.\n */\n startConversation(input: StartSupportChatInput): Promise<{\n ticket: SupportChatTicket;\n messages: SupportChatMessage[];\n }>;\n /** Send a message (text and/or files) on an existing ticket */\n sendMessage(\n ticketId: string,\n content: string,\n files?: File[]\n ): Promise<SupportChatMessage>;\n /** Messages of the ticket, oldest first */\n listMessages(ticketId: string): Promise<SupportChatMessage[]>;\n /** Listen to realtime events of the ticket; returns the unsubscribe */\n subscribe(\n ticketId: string,\n listener: (event: SupportChatEvent) => void\n ): () => void;\n /**\n * Save the name and/or e-mail the visitor typed in the contact form.\n * Without it the chat does not show the form (the AI request stays a text).\n */\n updateContact?(ticketId: string, contact: SupportChatContact): Promise<void>;\n /**\n * Conversation kept from an earlier page load that is still unresolved, so\n * a reload does not drop it. Null when there is none, when it was resolved\n * or when it belongs to another user (`email` of the logged user; omit for\n * a visitor).\n */\n restoreConversation?(input: { email?: string }): Promise<{\n ticket: SupportChatTicket;\n messages: SupportChatMessage[];\n } | null>;\n /** Stop keeping the conversation (resolved, or the chat started over) */\n forgetConversation?(): void;\n /**\n * Load a FAQ article of the institution. With it, the article an AI answer\n * is based on opens inside the chat instead of a new tab.\n */\n getArticle?(\n institutionId: string,\n articleId: string\n ): Promise<SupportChatArticle>;\n}\n\n/** Contact field the AI asks for */\nexport type SupportChatContactField = 'name' | 'email';\n\n/** Contact sent by the chat form */\nexport interface SupportChatContact {\n requesterName?: string;\n email?: string;\n}\n\n/**\n * Feature flags of the widget\n */\nexport interface SupportChatFlags {\n /** Widget available at all (default true) */\n enabled?: boolean;\n /** Chat with the support (default true) */\n chat?: boolean;\n /** FAQ (reserved, not rendered yet) */\n faq?: boolean;\n /** AI first answer (reserved, not rendered yet) */\n ai?: boolean;\n /** ZENDESK hides the widget (the Zendesk messenger is used instead) */\n supportType?: SupportType;\n}\n\n/**\n * Visual customization of the widget\n */\nexport interface SupportChatTheme {\n /** Background of the button and the header (any CSS color) */\n primaryColor?: string;\n /** Corner of the screen (default bottom-right) */\n position?: 'bottom-right' | 'bottom-left';\n /** Header title (default \"Suporte\") */\n title?: string;\n}\n\n/**\n * Logged user (optional; a visitor has none)\n */\nexport interface SupportChatUser {\n name?: string;\n email?: string;\n}\n\n/**\n * HTTP client the API adapter needs (axios instances fit as they are)\n */\nexport interface SupportChatHttpClient {\n get: <T>(\n url: string,\n config?: { headers?: Record<string, string> }\n ) => Promise<{ data: T }>;\n post: <T>(\n url: string,\n data?: unknown,\n config?: { headers?: Record<string, string> }\n ) => Promise<{ data: T }>;\n /** Used to save the visitor contact (PUT /support/guest/:id) */\n put?: <T>(\n url: string,\n data?: unknown,\n config?: { headers?: Record<string, string> }\n ) => Promise<{ data: T }>;\n}\n\n/**\n * Message as returned by the support REST routes\n */\nexport interface SupportChatAnswerApi {\n id: string;\n authorType: SupportChatAuthorRole;\n answer: string;\n createdAt: string;\n attachments?: SupportChatAttachment[];\n /** FAQ article an AI message is based on */\n faqArticleId?: string | null;\n faqArticleTitle?: string | null;\n /** Contact fields an AI message asks for */\n contactRequest?: SupportChatContactField[] | null;\n}\n"]}
@@ -6,10 +6,10 @@ import {
6
6
  import {
7
7
  QuestionsBars,
8
8
  RateCell
9
- } from "./chunk-B4C5DWAP.mjs";
9
+ } from "./chunk-MMMJDO75.mjs";
10
10
  import {
11
11
  TableProvider
12
- } from "./chunk-HBZHWSMZ.mjs";
12
+ } from "./chunk-D46PJSQ7.mjs";
13
13
  import {
14
14
  renderSubjectsCell
15
15
  } from "./chunk-JF753XAE.mjs";
@@ -265,4 +265,4 @@ function KnowledgePerformanceCard({
265
265
  export {
266
266
  KnowledgePerformanceCard
267
267
  };
268
- //# sourceMappingURL=chunk-L52NFNFK.mjs.map
268
+ //# sourceMappingURL=chunk-5VC4BK65.mjs.map
@@ -15,7 +15,7 @@ var _chunk2RIMYVDKjs = require('./chunk-2RIMYVDK.js');
15
15
  var _chunkZHPSF6BPjs = require('./chunk-ZHPSF6BP.js');
16
16
 
17
17
 
18
- var _chunk4RD55E4Rjs = require('./chunk-4RD55E4R.js');
18
+ var _chunkVWLWSO4Jjs = require('./chunk-VWLWSO4J.js');
19
19
 
20
20
 
21
21
  var _chunkKRYBXYOBjs = require('./chunk-KRYBXYOB.js');
@@ -468,7 +468,7 @@ var QuizMultipleChoice = ({ paddingBottom }) => {
468
468
  return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [
469
469
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, QuizSubTitle, { subTitle: "Alternativas" }),
470
470
  /* @__PURE__ */ _jsxruntime.jsx.call(void 0, QuizContainer, { className: _chunkTN3AYOMVjs.cn.call(void 0, "", paddingBottom), children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "space-y-4", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0,
471
- _chunk4RD55E4Rjs.MultipleChoiceList,
471
+ _chunkVWLWSO4Jjs.MultipleChoiceList,
472
472
  {
473
473
  labelledBy: a11yIds.titleId,
474
474
  describedBy: a11yIds.statementId,
@@ -1339,4 +1339,4 @@ var QuizImageQuestion = ({ paddingBottom }) => {
1339
1339
 
1340
1340
 
1341
1341
  exports.ImageAnswerView = ImageAnswerView; exports.DEFAULT_IMAGE_TOLERANCE = DEFAULT_IMAGE_TOLERANCE; exports.parseImageCorrectPoint = parseImageCorrectPoint; exports.parseImageStudentPoint = parseImageStudentPoint; exports.getStatusBadge = getStatusBadge; exports.getStatusStyles = getStatusStyles; exports.QuizAlternative = QuizAlternative; exports.QuizMultipleChoice = QuizMultipleChoice; exports.QuizDissertative = QuizDissertative; exports.QuizTrueOrFalse = QuizTrueOrFalse; exports.QuizConnectDots = QuizConnectDots; exports.QuizFill = QuizFill; exports.QuizImageQuestion = QuizImageQuestion;
1342
- //# sourceMappingURL=chunk-B67RLIUM.js.map
1342
+ //# sourceMappingURL=chunk-72D7G6YQ.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/chunk-B67RLIUM.js","../src/components/shared/ImageAnswerView.tsx","../src/components/Quiz/QuizContent.tsx","../src/utils/image/imageAnswer.utils.ts"],"names":["jsx","Fragment","jsxs"],"mappings":"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","file":"/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/chunk-B67RLIUM.js","sourcesContent":[null,"import Text from '../Text/Text';\nimport { cn } from '../../utils/utils';\n\n/** A point on the image, in percentage points of its width/height (0-100). */\nexport interface ImagePoint {\n x: number;\n y: number;\n}\n\nexport interface ImageAnswerViewProps {\n /** URL of the image the student had to click on */\n readonly imageUrl: string;\n /** The answer key point. Null when the question carries no key. */\n readonly correctPoint: ImagePoint | null;\n /** Where the student clicked. Null when the question was left blank. */\n readonly studentPoint: ImagePoint | null;\n /**\n * Radius within which a click counts as correct, in the same percentage\n * space. Comes from the backend so the drawn circle matches the grade.\n */\n readonly toleranceRadius: number;\n /** Whether the answer key may be revealed (false while pending) */\n readonly showCorrectness?: boolean;\n}\n\n/** Describe a point for screen readers. */\nconst describePoint = (point: ImagePoint): string =>\n `${Math.round(point.x)}% da esquerda, ${Math.round(point.y)}% do topo`;\n\n/** Euclidean distance between two points in the 0-100 space. */\nconst distanceBetween = (a: ImagePoint, b: ImagePoint): number =>\n Math.hypot(a.x - b.x, a.y - b.y);\n\n/**\n * Read-only view of an image question: the image, the answer key area and where\n * the student clicked.\n *\n * Shared by the activity correction modal, the simulation detail modal and the\n * student's result screen, which used to carry three divergent copies of this —\n * one of them drawing a mock image with a hardcoded answer area, and each one\n * deciding correctness with a different radius of its own. Here there is a\n * single rule, and it is the backend's: a click is correct when its distance to\n * the correct point is at most `toleranceRadius`.\n *\n * The target circle is drawn with the diameter of that tolerance, so what the\n * reader sees is what decided the grade.\n *\n * @param props - Component props\n * @returns JSX element\n *\n * @example\n * ```tsx\n * <ImageAnswerView\n * imageUrl={question.additionalContent ?? ''}\n * correctPoint={{ x: 50, y: 30 }}\n * studentPoint={{ x: 52, y: 28 }}\n * toleranceRadius={10}\n * />\n * ```\n */\nexport const ImageAnswerView = ({\n imageUrl,\n correctPoint,\n studentPoint,\n toleranceRadius,\n showCorrectness = true,\n}: ImageAnswerViewProps) => {\n const canShowCorrectness = showCorrectness && correctPoint !== null;\n const isCorrect =\n correctPoint !== null &&\n studentPoint !== null &&\n distanceBetween(studentPoint, correctPoint) <= toleranceRadius;\n\n /** Full spoken summary of the question, the only non-visual access to it. */\n const spatialSummary = (): string => {\n if (!correctPoint) {\n return studentPoint\n ? `Resposta do aluno em ${describePoint(studentPoint)}.`\n : 'Nenhuma resposta do aluno fornecida.';\n }\n\n const correctDescription = describePoint(correctPoint);\n if (!studentPoint) {\n return `Área correta localizada em ${correctDescription}. Nenhuma resposta do aluno fornecida.`;\n }\n\n const deltaX = studentPoint.x - correctPoint.x;\n const deltaY = studentPoint.y - correctPoint.y;\n let direction: string;\n if (Math.abs(deltaX) > Math.abs(deltaY)) {\n direction = deltaX > 0 ? 'à direita' : 'à esquerda';\n } else {\n direction = deltaY > 0 ? 'abaixo' : 'acima';\n }\n\n const distance = Math.round(distanceBetween(studentPoint, correctPoint));\n const verdict = isCorrect\n ? 'A resposta está dentro da área de tolerância e é considerada correta.'\n : 'A resposta está fora da área de tolerância e é considerada incorreta.';\n\n return `Área correta localizada em ${correctDescription}. Resposta do aluno em ${describePoint(studentPoint)}. A resposta do aluno está ${distance}% de distância ${direction} da área correta. ${verdict}`;\n };\n\n return (\n <div className=\"pt-2 space-y-4\">\n <div className=\"flex items-center gap-4 text-xs\">\n {canShowCorrectness && (\n <div className=\"flex items-center gap-2\">\n <div className=\"w-3 h-3 rounded-full bg-indicator-primary/70 border border-[#F8CC2E]\" />\n <Text size=\"sm\" weight=\"normal\" color=\"text-text-600\">\n Área correta\n </Text>\n </div>\n )}\n {canShowCorrectness && studentPoint && (\n <div className=\"flex items-center gap-2\">\n <div\n className={cn(\n 'w-3 h-3 rounded-full border border-white',\n isCorrect ? 'bg-success-600/70' : 'bg-indicator-error/70'\n )}\n />\n <Text size=\"sm\" weight=\"normal\" color=\"text-text-600\">\n {isCorrect ? 'Resposta correta' : 'Resposta incorreta'}\n </Text>\n </div>\n )}\n </div>\n\n <div className=\"relative w-full\">\n <div className=\"sr-only\">{spatialSummary()}</div>\n\n {imageUrl ? (\n <img\n src={imageUrl}\n alt={\n correctPoint\n ? `Questão de imagem com área correta localizada em ${describePoint(correctPoint)}`\n : 'Questão de imagem'\n }\n className=\"w-full h-auto rounded-md\"\n />\n ) : (\n <div className=\"rounded-md border border-border-100 bg-background-50 p-6 text-center\">\n <Text size=\"sm\" className=\"text-text-600\">\n Imagem da questão indisponível\n </Text>\n </div>\n )}\n\n {canShowCorrectness && correctPoint && (\n <div\n aria-hidden=\"true\"\n data-testid=\"image-correct-area\"\n className=\"absolute rounded-full bg-indicator-primary/70 border-4 border-[#F8CC2E] pointer-events-none -translate-x-1/2 -translate-y-1/2\"\n style={{\n // Diameter = twice the tolerance, so the circle IS the pass mark.\n width: `${toleranceRadius * 2}%`,\n aspectRatio: '1 / 1',\n left: `${correctPoint.x}%`,\n top: `${correctPoint.y}%`,\n }}\n />\n )}\n\n {studentPoint && (\n <div\n aria-hidden=\"true\"\n data-testid=\"image-student-point\"\n className={cn(\n 'absolute rounded-full border-4 pointer-events-none -translate-x-1/2 -translate-y-1/2',\n canShowCorrectness &&\n isCorrect &&\n 'bg-success-600/70 border-white',\n canShowCorrectness &&\n !isCorrect &&\n 'bg-indicator-error/70 border-white',\n !canShowCorrectness && 'bg-indicator-primary/70 border-[#F8CC2E]'\n )}\n style={{\n minWidth: '20px',\n maxWidth: '40px',\n width: '4%',\n aspectRatio: '1 / 1',\n left: `${studentPoint.x}%`,\n top: `${studentPoint.y}%`,\n }}\n />\n )}\n </div>\n </div>\n );\n};\n\nexport default ImageAnswerView;\n","import {\n Fragment,\n forwardRef,\n MouseEvent,\n ReactNode,\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport { ANSWER_STATUS, useQuizStore } from './useQuizStore';\nimport { QuizVariant } from './Quiz.types';\nimport CorrectionSourceTag from './CorrectionSourceTag';\nimport { TrueFalseEnum } from '../../enums/Quiz';\nimport {\n prependLetterToHtml,\n getTrueOrFalseOptionState,\n getQuestionA11yIds,\n shuffleWithSeed,\n} from './Quiz.utils';\nimport { cn } from '../../utils/utils';\nimport { stripHtmlTags } from '../../utils/stringUtils';\nimport Select, {\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '../Select/Select';\nimport TextArea from '../TextArea/TextArea';\nimport { AlternativesList } from '../Alternative/Alternative';\nimport { OptionStatus } from '../../enums/Options';\nimport { MultipleChoiceList } from '../MultipleChoice/MultipleChoice';\nimport Badge from '../Badge/Badge';\nimport { CheckCircleIcon } from '@phosphor-icons/react/dist/csr/CheckCircle';\nimport { XCircleIcon } from '@phosphor-icons/react/dist/csr/XCircle';\nimport ImageQuestion from '../../assets/img/mock-image-question.png';\nimport { ImageAnswerView } from '../shared/ImageAnswerView';\nimport {\n DEFAULT_IMAGE_TOLERANCE,\n parseImageCorrectPoint,\n parseImageStudentPoint,\n} from '../../utils/image/imageAnswer.utils';\nimport Text from '../Text/Text';\nimport { HtmlMathRenderer } from '../HtmlMathRenderer';\nexport const getStatusBadge = (status?: OptionStatus) => {\n switch (status) {\n case OptionStatus.CORRECT:\n return (\n <Badge\n variant=\"solid\"\n action=\"success\"\n iconLeft={<CheckCircleIcon aria-hidden=\"true\" />}\n >\n Resposta correta\n </Badge>\n );\n case OptionStatus.INCORRECT:\n return (\n <Badge\n variant=\"solid\"\n action=\"error\"\n iconLeft={<XCircleIcon aria-hidden=\"true\" />}\n >\n Resposta incorreta\n </Badge>\n );\n default:\n return null;\n }\n};\n\nexport const getStatusStyles = (variantCorrect?: OptionStatus) => {\n switch (variantCorrect) {\n case OptionStatus.CORRECT:\n return 'bg-success-background border-success-300';\n case OptionStatus.INCORRECT:\n return 'bg-error-background border-error-300';\n default:\n return '';\n }\n};\n\n/**\n * Normalizes parsed answer data to Record<string, string> format.\n * Handles legacy array format [{ optionId, selectedValue }] by converting to { [optionId]: selectedValue }.\n */\nconst normalizeAnswerData = (parsed: unknown): Record<string, string> => {\n // Handle legacy array format: [{ optionId: string, selectedValue: string }]\n if (Array.isArray(parsed)) {\n const result: Record<string, string> = {};\n for (const item of parsed) {\n if (\n item &&\n typeof item === 'object' &&\n 'optionId' in item &&\n 'selectedValue' in item\n ) {\n result[item.optionId as string] = item.selectedValue as string;\n }\n }\n return result;\n }\n\n // Handle expected object format: { [optionId]: selectedValue }\n if (parsed && typeof parsed === 'object') {\n return parsed as Record<string, string>;\n }\n\n return {};\n};\n\n/**\n * Parses JSON answers from stored answer string.\n * In result mode, uses persisted results. Otherwise, uses current draft answers.\n * Normalizes legacy array format to expected Record<string, string> map.\n */\nconst parseStoredAnswers = (\n variant: QuizVariant,\n resultAnswer: string | null | undefined,\n currentAnswer: string | null | undefined\n): Record<string, string> => {\n if (variant === QuizVariant.RESULT) {\n if (!resultAnswer) return {};\n try {\n const parsed = JSON.parse(resultAnswer);\n return normalizeAnswerData(parsed);\n } catch {\n return {};\n }\n }\n if (currentAnswer) {\n try {\n const parsed = JSON.parse(currentAnswer);\n return normalizeAnswerData(parsed);\n } catch {\n return {};\n }\n }\n return {};\n};\n\n/**\n * Converts isCorrect boolean to status variant string.\n * Returns undefined for null/unanswered to keep neutral styling.\n */\nconst getAnswerStatus = (\n isCorrect: boolean | null\n): OptionStatus | undefined => {\n if (isCorrect === true) return OptionStatus.CORRECT;\n if (isCorrect === false) return OptionStatus.INCORRECT;\n return undefined;\n};\n\ninterface QuizVariantInterface {\n paddingBottom?: string;\n}\n\nconst QuizSubTitle = forwardRef<HTMLDivElement, { subTitle: string }>(\n ({ subTitle, ...props }, ref) => {\n return (\n <div className=\"px-4 pb-2 pt-6\" {...props} ref={ref}>\n {/* Subtítulo de seção da questão (ex.: \"Alternativas\"): h3 para\n entrar na navegação por cabeçalhos abaixo dos h1/h2 da tela. */}\n <Text as=\"h3\" size=\"lg\" weight=\"bold\" color=\"text-text-950\">\n {subTitle}\n </Text>\n </div>\n );\n }\n);\n\nconst QuizContainer = forwardRef<\n HTMLDivElement,\n { children: ReactNode; className?: string }\n>(({ children, className, ...props }, ref) => {\n return (\n <div\n ref={ref}\n className={cn(\n 'bg-background rounded-t-xl px-4 pt-4 pb-[80px] h-auto flex flex-col gap-4 mb-auto',\n className\n )}\n {...props}\n >\n {children}\n </div>\n );\n});\n\nconst QuizAlternative = ({ paddingBottom }: QuizVariantInterface) => {\n const {\n getCurrentQuestion,\n selectAnswer,\n getQuestionResultByQuestionId,\n getCurrentAnswer,\n variant,\n } = useQuizStore();\n const currentQuestion = getCurrentQuestion();\n const a11yIds = getQuestionA11yIds(currentQuestion?.id);\n const currentQuestionResult = getQuestionResultByQuestionId(\n currentQuestion?.id || ''\n );\n\n const currentAnswer = getCurrentAnswer();\n const alternatives = currentQuestion?.options?.map((option) => {\n let status: OptionStatus = OptionStatus.NEUTRAL;\n if (variant === QuizVariant.RESULT) {\n const isCorrectOption =\n currentQuestionResult?.options?.find((op) => op.id === option.id)\n ?.isCorrect || false;\n\n const isSelected = currentQuestionResult?.selectedOptions.some(\n (selectedOption) => selectedOption.optionId === option.id\n );\n\n // Only show correct/incorrect status if answer is not pending evaluation\n const shouldShowCorrectAnswers =\n currentQuestionResult?.answerStatus !==\n ANSWER_STATUS.PENDENTE_AVALIACAO &&\n currentQuestionResult?.answerStatus !== ANSWER_STATUS.NAO_RESPONDIDO;\n\n if (shouldShowCorrectAnswers) {\n if (isCorrectOption) {\n status = OptionStatus.CORRECT;\n } else if (isSelected && !isCorrectOption) {\n status = OptionStatus.INCORRECT;\n } else {\n status = OptionStatus.NEUTRAL;\n }\n } else {\n // When pending evaluation, show all options as neutral\n status = OptionStatus.NEUTRAL;\n }\n }\n\n return {\n label: option.option,\n value: option.id,\n status: status,\n };\n });\n\n if (!alternatives)\n return (\n <div>\n <p>Não há Alternativas</p>\n </div>\n );\n\n return (\n <>\n <QuizSubTitle subTitle=\"Alternativas\" />\n\n <QuizContainer className={cn('', paddingBottom)}>\n <div className=\"space-y-4\">\n <AlternativesList\n labelledBy={a11yIds.titleId}\n describedBy={a11yIds.statementId}\n mode={variant === QuizVariant.DEFAULT ? 'interactive' : 'readonly'}\n key={`question-${currentQuestion?.id || '1'}`}\n name={`question-${currentQuestion?.id || '1'}`}\n layout=\"compact\"\n alternatives={alternatives}\n value={\n variant === QuizVariant.RESULT\n ? currentQuestionResult?.selectedOptions[0]?.optionId || ''\n : currentAnswer?.optionId || ''\n }\n selectedValue={\n variant === QuizVariant.RESULT\n ? currentQuestionResult?.selectedOptions[0]?.optionId || ''\n : currentAnswer?.optionId || ''\n }\n onValueChange={(value) => {\n if (currentQuestion) {\n selectAnswer(currentQuestion.id, value);\n }\n }}\n />\n </div>\n </QuizContainer>\n </>\n );\n};\n\nconst QuizMultipleChoice = ({ paddingBottom }: QuizVariantInterface) => {\n const {\n getCurrentQuestion,\n selectMultipleAnswer,\n getQuestionResultByQuestionId,\n variant,\n } = useQuizStore();\n const currentQuestion = getCurrentQuestion();\n const a11yIds = getQuestionA11yIds(currentQuestion?.id);\n // Derive the current question's answers from the stable `userAnswers` slice\n // instead of calling getAllCurrentAnswer(), which allocates a fresh array on\n // every render. That fresh identity propagated through the memos below and,\n // combined with the store→prop→local-state mirror in MultipleChoiceList,\n // caused a \"Maximum update depth exceeded\" loop on /questionario.\n const userAnswers = useQuizStore((state) => state.userAnswers);\n const allCurrentAnswers = useMemo(\n () =>\n currentQuestion\n ? userAnswers.filter(\n (answer) => answer.questionId === currentQuestion.id\n )\n : [],\n [userAnswers, currentQuestion]\n );\n const currentQuestionResult = getQuestionResultByQuestionId(\n currentQuestion?.id || ''\n );\n // Use ref to track previous values and prevent unnecessary updates\n const prevSelectedValuesRef = useRef<string[]>([]);\n const prevQuestionIdRef = useRef<string>('');\n\n // Memoize the answer IDs to prevent unnecessary re-renders\n // For MULTIPLA_ESCOLHA, we now have a single answer entry with selectedOptionIds array\n const allCurrentAnswerIds = useMemo(() => {\n if (allCurrentAnswers && allCurrentAnswers.length > 0) {\n // Prefer any row already using the new format\n const answerWithSelectedOptionIds = allCurrentAnswers.find((answer) =>\n Array.isArray(answer.selectedOptionIds)\n );\n if (answerWithSelectedOptionIds) {\n return answerWithSelectedOptionIds.selectedOptionIds ?? [];\n }\n // Fallback to old format (multiple entries with optionId)\n return allCurrentAnswers.map((answer) => answer.optionId);\n }\n return [];\n }, [allCurrentAnswers]);\n\n // Memoize the selected values to prevent infinite loops\n const selectedValues = useMemo(() => {\n return allCurrentAnswerIds?.filter((id): id is string => id !== null) || [];\n }, [allCurrentAnswerIds]);\n\n // Only update selectedValues if they actually changed or question changed\n const stableSelectedValues = useMemo(() => {\n // In result mode, always use selectedOptions from the question result\n if (variant === QuizVariant.RESULT) {\n return (\n currentQuestionResult?.selectedOptions?.map((op) => op.optionId) || []\n );\n }\n\n const currentQuestionId = currentQuestion?.id || '';\n const hasQuestionChanged = prevQuestionIdRef.current !== currentQuestionId;\n\n if (hasQuestionChanged) {\n prevQuestionIdRef.current = currentQuestionId;\n prevSelectedValuesRef.current = selectedValues;\n return selectedValues;\n }\n\n // Only update if values actually changed\n const hasValuesChanged =\n JSON.stringify(prevSelectedValuesRef.current) !==\n JSON.stringify(selectedValues);\n if (hasValuesChanged) {\n prevSelectedValuesRef.current = selectedValues;\n return selectedValues;\n }\n\n return prevSelectedValuesRef.current;\n }, [\n selectedValues,\n currentQuestion?.id,\n variant,\n currentQuestionResult?.selectedOptions,\n ]);\n\n // Memoize the callback to prevent unnecessary re-renders\n const handleSelectedValues = useCallback(\n (values: string[]) => {\n if (currentQuestion) {\n selectMultipleAnswer(currentQuestion.id, values);\n }\n },\n [currentQuestion, selectMultipleAnswer]\n );\n\n // Create a stable key to force re-mount when question changes\n const questionKey = useMemo(\n () => `question-${currentQuestion?.id || '1'}`,\n [currentQuestion?.id]\n );\n const choices = currentQuestion?.options?.map((option) => {\n let status: OptionStatus = OptionStatus.NEUTRAL;\n\n if (variant === QuizVariant.RESULT) {\n const isCorrectOption =\n currentQuestionResult?.options?.find((op) => op.id === option.id)\n ?.isCorrect || false;\n\n const isSelected = currentQuestionResult?.selectedOptions?.some(\n (op) => op.optionId === option.id\n );\n\n // Only show correct/incorrect status if answer is not pending evaluation\n const shouldShowCorrectAnswers =\n currentQuestionResult?.answerStatus !==\n ANSWER_STATUS.PENDENTE_AVALIACAO &&\n currentQuestionResult?.answerStatus !== ANSWER_STATUS.NAO_RESPONDIDO;\n\n if (shouldShowCorrectAnswers) {\n if (isCorrectOption) {\n status = OptionStatus.CORRECT;\n } else if (isSelected && !isCorrectOption) {\n status = OptionStatus.INCORRECT;\n } else {\n status = OptionStatus.NEUTRAL;\n }\n } else {\n // When pending evaluation, show all options as neutral\n status = OptionStatus.NEUTRAL;\n }\n }\n\n return {\n label: option.option,\n value: option.id,\n status: status,\n };\n });\n\n if (!choices)\n return (\n <div>\n <p>Não há Escolhas Multiplas</p>\n </div>\n );\n return (\n <>\n <QuizSubTitle subTitle=\"Alternativas\" />\n\n <QuizContainer className={cn('', paddingBottom)}>\n <div className=\"space-y-4\">\n <MultipleChoiceList\n labelledBy={a11yIds.titleId}\n describedBy={a11yIds.statementId}\n choices={choices}\n key={questionKey}\n name={questionKey}\n selectedValues={stableSelectedValues}\n onHandleSelectedValues={handleSelectedValues}\n mode={variant === QuizVariant.DEFAULT ? 'interactive' : 'readonly'}\n />\n </div>\n </QuizContainer>\n </>\n );\n};\n\nconst QuizDissertative = ({ paddingBottom }: QuizVariantInterface) => {\n const {\n getCurrentQuestion,\n getCurrentAnswer,\n selectDissertativeAnswer,\n getQuestionResultByQuestionId,\n variant,\n getDissertativeCharLimit,\n } = useQuizStore();\n\n const currentQuestion = getCurrentQuestion();\n const a11yIds = getQuestionA11yIds(currentQuestion?.id);\n const currentQuestionResult = getQuestionResultByQuestionId(\n currentQuestion?.id || ''\n );\n\n const currentAnswer = getCurrentAnswer();\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n const charLimit = getDissertativeCharLimit();\n\n const handleAnswerChange = (value: string) => {\n if (currentQuestion) {\n selectDissertativeAnswer(currentQuestion.id, value);\n }\n };\n\n // Auto-resize function\n const adjustTextareaHeight = useCallback(() => {\n if (textareaRef.current) {\n textareaRef.current.style.height = 'auto';\n const scrollHeight = textareaRef.current.scrollHeight;\n const minHeight = 120; // 120px minimum height\n const maxHeight = 400; // 400px maximum height\n const newHeight = Math.min(Math.max(scrollHeight, minHeight), maxHeight);\n textareaRef.current.style.height = `${newHeight}px`;\n }\n }, []);\n\n // Adjust height when currentAnswer changes\n useEffect(() => {\n adjustTextareaHeight();\n }, [currentAnswer, adjustTextareaHeight]);\n\n if (!currentQuestion) {\n return (\n <div className=\"space-y-4\">\n <p className=\"text-text-600 text-md\">Nenhuma questão disponível</p>\n </div>\n );\n }\n\n const localAnswer =\n (variant == 'result'\n ? currentQuestionResult?.answer\n : currentAnswer?.answer) || '';\n return (\n <>\n <div className=\"flex items-center justify-between gap-2 flex-wrap\">\n <QuizSubTitle subTitle=\"Resposta\" />\n {/* Quem corrigiu esta dissertativa. Fica aqui, ao lado da resposta, e\n não só dentro do modal de feedback: o aluno tem que saber que a\n correção veio da IA sem precisar abrir nada. Só aparece no\n resultado — durante a prova não há correção nenhuma. */}\n {variant === QuizVariant.RESULT && (\n <CorrectionSourceTag\n correctionSource={currentQuestionResult?.correctionSource}\n aiCorrectionStatus={currentQuestionResult?.aiCorrectionStatus}\n />\n )}\n </div>\n\n <QuizContainer className={cn(variant != 'result' && paddingBottom)}>\n <div className=\"space-y-4 max-h-[600px] overflow-y-auto\">\n {variant === QuizVariant.DEFAULT ? (\n <div className=\"space-y-4\">\n <TextArea\n ref={textareaRef}\n aria-labelledby={a11yIds.titleId}\n aria-describedby={a11yIds.statementId}\n placeholder=\"Escreva sua resposta\"\n value={localAnswer}\n onChange={(e) => handleAnswerChange(e.target.value)}\n rows={4}\n className=\"min-h-[120px] max-h-[400px] resize-none overflow-y-auto\"\n maxLength={charLimit}\n showCharacterCount={!!charLimit}\n />\n </div>\n ) : (\n <div className=\"space-y-4\">\n <p className=\"text-text-600 text-md whitespace-pre-wrap\">\n {localAnswer || 'Nenhuma resposta fornecida'}\n </p>\n </div>\n )}\n </div>\n </QuizContainer>\n\n {/* The teacher's comment is rendered by `TeacherQuestionComment`, mounted\n once in `QuizContent` for every question type. This block used to\n duplicate it here, so an essay showed the same text twice. */}\n </>\n );\n};\n\nconst QuizTrueOrFalse = ({ paddingBottom }: QuizVariantInterface) => {\n const {\n variant,\n getCurrentQuestion,\n getCurrentAnswer,\n selectDissertativeAnswer,\n getQuestionResultByQuestionId,\n getUserAnswerByQuestionId,\n } = useQuizStore();\n\n const currentQuestion = getCurrentQuestion();\n const currentAnswer = getCurrentAnswer();\n const currentQuestionResult = getQuestionResultByQuestionId(\n currentQuestion?.id || ''\n );\n\n // Get the options - prefer persisted result options in result mode, fallback to live question\n const options = useMemo(() => {\n if (variant === QuizVariant.RESULT && currentQuestionResult?.options) {\n return currentQuestionResult.options;\n }\n return currentQuestion?.options || [];\n }, [variant, currentQuestionResult?.options, currentQuestion?.options]);\n\n // Parse stored answers from JSON string, handling both object and legacy array formats\n const parsedAnswers: Record<string, string> = useMemo(() => {\n return parseStoredAnswers(\n variant,\n currentQuestionResult?.answer,\n currentAnswer?.answer\n );\n }, [variant, currentQuestionResult?.answer, currentAnswer?.answer]);\n\n // Local state to track answers: { \"optionId\": \"V\" | \"F\" }\n const [localAnswers, setLocalAnswers] =\n useState<Record<string, string>>(parsedAnswers);\n\n // Sync local answers when question changes or parsed answers update\n useEffect(() => {\n setLocalAnswers(parsedAnswers);\n }, [parsedAnswers, currentQuestion?.id]);\n\n // Handle select change for an option\n const handleSelectChange = (optionId: string, value: string) => {\n if (!currentQuestion) return;\n\n // Get the LATEST answers directly from the store (not from closure)\n // This is critical to avoid stale closure issues when user selects options quickly\n const latestStoreAnswer = getUserAnswerByQuestionId(currentQuestion.id);\n let baseAnswers: Record<string, string> = {};\n if (latestStoreAnswer?.answer) {\n try {\n const parsed = JSON.parse(latestStoreAnswer.answer);\n baseAnswers = normalizeAnswerData(parsed);\n } catch {\n // Invalid JSON, start fresh\n }\n }\n\n // Merge: store answers + local state + new selection\n const mergedAnswers = {\n ...baseAnswers,\n ...localAnswers,\n [optionId]: value,\n };\n setLocalAnswers(mergedAnswers);\n\n // Save to store as JSON string\n selectDissertativeAnswer(currentQuestion.id, JSON.stringify(mergedAnswers));\n };\n\n // Get the current selection for an option\n const getOptionSelection = (optionId: string): string | undefined => {\n return localAnswers[optionId];\n };\n\n const getLetterByIndex = (index: number) => String.fromCodePoint(97 + index); // 97 = 'a' in ASCII\n\n const isDefaultVariant = variant === QuizVariant.DEFAULT;\n\n // Check if we should show correct/incorrect status\n const shouldShowStatus =\n currentQuestionResult?.answerStatus !== ANSWER_STATUS.PENDENTE_AVALIACAO &&\n currentQuestionResult?.answerStatus !== ANSWER_STATUS.NAO_RESPONDIDO;\n\n if (options.length === 0) {\n return (\n <QuizContainer className={cn('', paddingBottom)}>\n <Text size=\"sm\" className=\"text-text-500 italic\">\n Nenhuma opção disponível para esta questão\n </Text>\n </QuizContainer>\n );\n }\n\n return (\n <>\n <QuizSubTitle subTitle=\"Alternativas\" />\n\n <QuizContainer className={cn('', paddingBottom)}>\n <div className=\"flex flex-col gap-3.5\">\n {options.map((option, index) => {\n // Use helper to compute all option state\n const {\n hasAnswered,\n studentAnswer,\n correctAnswer,\n isStudentCorrect,\n variantCorrect,\n } = getTrueOrFalseOptionState(\n option.id,\n variant,\n currentQuestionResult,\n localAnswers\n );\n\n const contentWithLetter = prependLetterToHtml(\n getLetterByIndex(index),\n option.option\n );\n\n return (\n <section\n key={option.id || `option-${index}`}\n className=\"flex flex-col gap-2\"\n >\n <div\n className={cn(\n 'w-full grid grid-cols-[1fr_auto] items-start gap-4 p-2 rounded-md border',\n !isDefaultVariant && shouldShowStatus && hasAnswered\n ? getStatusStyles(variantCorrect)\n : 'border-transparent'\n )}\n >\n <HtmlMathRenderer\n content={contentWithLetter}\n className=\"text-text-900 text-sm\"\n />\n\n {isDefaultVariant ? (\n <Select\n size=\"medium\"\n value={getOptionSelection(option.id)}\n onValueChange={(value) =>\n handleSelectChange(option.id, value)\n }\n >\n <SelectTrigger className=\"w-[120px]\">\n <SelectValue placeholder=\"Selecione\" />\n </SelectTrigger>\n\n <SelectContent>\n <SelectItem value={TrueFalseEnum.VERDADEIRO}>\n Verdadeiro\n </SelectItem>\n <SelectItem value={TrueFalseEnum.FALSO}>\n Falso\n </SelectItem>\n </SelectContent>\n </Select>\n ) : (\n shouldShowStatus &&\n hasAnswered && <div>{getStatusBadge(variantCorrect)}</div>\n )}\n </div>\n\n {!isDefaultVariant && shouldShowStatus && (\n <span className=\"flex flex-row gap-2 items-center flex-wrap\">\n {hasAnswered ? (\n <>\n <Text size=\"2xs\" className=\"text-text-800\">\n Resposta selecionada: {studentAnswer}\n </Text>\n {!isStudentCorrect && (\n <Text size=\"2xs\" className=\"text-text-800\">\n {/* Separador só visual: o leitor leria \"barra vertical\". */}\n <Text\n as=\"span\"\n size=\"2xs\"\n color=\"text-text-800\"\n aria-hidden=\"true\"\n >\n |{' '}\n </Text>\n Resposta correta: {correctAnswer}\n </Text>\n )}\n </>\n ) : (\n <Text size=\"2xs\" className=\"text-text-800\">\n Não respondida{' '}\n <Text\n as=\"span\"\n size=\"2xs\"\n color=\"text-text-800\"\n aria-hidden=\"true\"\n >\n |\n </Text>{' '}\n Resposta correta: {correctAnswer}\n </Text>\n )}\n </span>\n )}\n </section>\n );\n })}\n </div>\n </QuizContainer>\n </>\n );\n};\n\ninterface UserAnswer {\n option: string;\n dotOption: string | null;\n correctOption: string;\n isCorrect: boolean | null;\n}\n\nconst QuizConnectDots = ({ paddingBottom }: QuizVariantInterface) => {\n const {\n variant,\n getCurrentQuestion,\n getCurrentAnswer,\n selectDissertativeAnswer,\n getQuestionResultByQuestionId,\n getUserAnswerByQuestionId,\n } = useQuizStore();\n\n const currentQuestion = getCurrentQuestion();\n const currentQuestionResult = getQuestionResultByQuestionId(\n currentQuestion?.id || ''\n );\n const currentAnswer = getCurrentAnswer();\n\n // Extract options from the current question\n // Each option has: id, option (left column), correctValue (right column)\n const questionOptions = useMemo(() => {\n if (!currentQuestion?.options) return [];\n return currentQuestion.options;\n }, [currentQuestion?.options]);\n\n // Build dotsOptions from correctValue of each option (right column values)\n // Shuffle them so they don't appear in the same order as the left column\n const dotsOptions = useMemo(() => {\n const dots = questionOptions\n .map((opt) => ({ label: opt.correctValue || '' }))\n .filter((opt) => opt.label !== '');\n // Use question ID as seed for consistent shuffle\n return shuffleWithSeed(dots, currentQuestion?.id || '');\n }, [questionOptions, currentQuestion?.id]);\n\n // Build options for display (left column with correct answer mapping)\n const options = useMemo(() => {\n return questionOptions.map((opt) => ({\n id: opt.id,\n label: opt.option,\n correctOption: opt.correctValue || '',\n }));\n }, [questionOptions]);\n\n // Parse stored matching answers\n // In result mode, read from matchingAnswers array, fallback to answer JSON\n // In default mode, read from currentAnswer.answer JSON string\n const parsedAnswers: Record<string, string> = useMemo(() => {\n if (\n variant === QuizVariant.RESULT &&\n currentQuestionResult?.matchingAnswers\n ) {\n // Convert matchingAnswers array to Record<optionId, selectedValue>\n const answers: Record<string, string> = {};\n for (const match of currentQuestionResult.matchingAnswers) {\n answers[match.optionId] = match.selectedValue;\n }\n return answers;\n }\n // Fallback to answer JSON string (for older/partially migrated submissions)\n return parseStoredAnswers(\n variant,\n currentQuestionResult?.answer,\n currentAnswer?.answer\n );\n }, [\n variant,\n currentQuestionResult?.matchingAnswers,\n currentQuestionResult?.answer,\n currentAnswer?.answer,\n ]);\n\n const [userAnswers, setUserAnswers] = useState<UserAnswer[]>([]);\n\n // Initialize userAnswers from options and stored answers\n useEffect(() => {\n if (options.length > 0) {\n setUserAnswers(\n options.map((option) => {\n const storedValue = parsedAnswers[option.id] || null;\n return {\n option: option.label,\n dotOption: storedValue,\n correctOption: option.correctOption,\n isCorrect: storedValue\n ? storedValue === option.correctOption\n : null,\n };\n })\n );\n }\n }, [options, parsedAnswers]);\n\n const handleSelectDot = (optionIndex: number, dotValue: string) => {\n const option = options[optionIndex];\n if (!option || !currentQuestion) return;\n\n // Update local state\n setUserAnswers((prev) => {\n const next = [...prev];\n next[optionIndex] = {\n option: option.label,\n dotOption: dotValue,\n correctOption: option.correctOption,\n isCorrect: dotValue ? dotValue === option.correctOption : null,\n };\n return next;\n });\n\n // Get the LATEST answers directly from the store (not from closure)\n const latestStoreAnswer = getUserAnswerByQuestionId(currentQuestion.id);\n let baseAnswers: Record<string, string> = {};\n if (latestStoreAnswer?.answer) {\n try {\n const parsed = JSON.parse(latestStoreAnswer.answer);\n baseAnswers = normalizeAnswerData(parsed);\n } catch {\n // Invalid JSON, start fresh\n }\n }\n\n // Persist to shared store as JSON\n const newAnswers = {\n ...baseAnswers,\n ...parsedAnswers,\n [option.id]: dotValue,\n };\n selectDissertativeAnswer(currentQuestion.id, JSON.stringify(newAnswers));\n };\n\n const getLetterByIndex = (index: number) => String.fromCodePoint(97 + index); // 'a', 'b', 'c'...\n\n const isDefaultVariant = variant === QuizVariant.DEFAULT;\n const assignedDots = new Set(\n userAnswers.map((a) => a.dotOption).filter(Boolean)\n );\n\n if (options.length === 0) {\n return (\n <QuizContainer className={cn('', paddingBottom)}>\n <Text size=\"sm\" className=\"text-text-500 italic\">\n Nenhuma opção de relacionamento disponível\n </Text>\n </QuizContainer>\n );\n }\n\n return (\n <>\n <QuizSubTitle subTitle=\"Alternativas\" />\n\n <QuizContainer className={cn('', paddingBottom)}>\n <div className=\"flex flex-col gap-3.5\">\n {options.map((option, index) => {\n const answer = userAnswers[index] || {\n option: option.label,\n dotOption: null,\n correctOption: option.correctOption,\n isCorrect: null,\n };\n const variantCorrect = getAnswerStatus(answer.isCorrect);\n return (\n <section key={option.id} className=\"flex flex-col gap-2\">\n <div\n className={cn(\n 'grid grid-cols-[1fr_auto] items-center gap-4 p-2 rounded-md',\n !isDefaultVariant && variantCorrect\n ? getStatusStyles(variantCorrect)\n : ''\n )}\n >\n <HtmlMathRenderer\n content={prependLetterToHtml(\n getLetterByIndex(index),\n option.label\n )}\n className=\"text-text-900 text-sm\"\n />\n\n {isDefaultVariant ? (\n <Select\n size=\"medium\"\n value={answer.dotOption || undefined}\n onValueChange={(value) => handleSelectDot(index, value)}\n >\n <SelectTrigger\n className=\"w-[180px] overflow-hidden truncate\"\n title={\n answer.dotOption\n ? stripHtmlTags(answer.dotOption)\n : undefined\n }\n >\n <SelectValue placeholder=\"Selecione opção\" />\n </SelectTrigger>\n\n <SelectContent>\n {dotsOptions\n .filter(\n (dot) =>\n !assignedDots.has(dot.label) ||\n answer.dotOption === dot.label\n )\n .map((dot) => (\n <SelectItem\n key={dot.label}\n value={dot.label}\n title={stripHtmlTags(dot.label)}\n truncate\n >\n {stripHtmlTags(dot.label)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n ) : (\n <div>\n {answer.isCorrect === null\n ? null\n : getStatusBadge(variantCorrect)}\n </div>\n )}\n </div>\n\n {!isDefaultVariant && (\n <span className=\"flex flex-row gap-2 items-center\">\n <Text size=\"2xs\" className=\"text-text-800\">\n Resposta selecionada:{' '}\n {answer.dotOption\n ? stripHtmlTags(answer.dotOption)\n : 'Nenhuma'}\n </Text>\n {answer.isCorrect === false && (\n <Text size=\"2xs\" className=\"text-text-800\">\n Resposta correta: {stripHtmlTags(answer.correctOption)}\n </Text>\n )}\n </span>\n )}\n </section>\n );\n })}\n </div>\n </QuizContainer>\n </>\n );\n};\n\nconst QuizFill = ({ paddingBottom }: QuizVariantInterface) => {\n const {\n getCurrentQuestion,\n getQuestionResultByQuestionId,\n getCurrentAnswer,\n selectDissertativeAnswer,\n variant,\n getUserAnswerByQuestionId,\n } = useQuizStore();\n\n const currentQuestion = getCurrentQuestion();\n const currentQuestionResult = getQuestionResultByQuestionId(\n currentQuestion?.id || ''\n );\n const currentAnswer = getCurrentAnswer();\n\n const baseId = useId();\n\n // Get the additionalContent from the question (contains text with {optionId} placeholders)\n // In result mode, prefer the persisted snapshot to match parsedAnswers\n const additionalContent =\n variant === QuizVariant.RESULT\n ? currentQuestionResult?.additionalContent ||\n currentQuestion?.additionalContent ||\n ''\n : currentQuestion?.additionalContent || '';\n\n // Get the options from the question\n // In result mode, prefer the persisted snapshot to match parsedAnswers\n const questionOptions =\n variant === QuizVariant.RESULT\n ? currentQuestionResult?.options || currentQuestion?.options || []\n : currentQuestion?.options || [];\n\n // Shuffled options for dropdown display (consistent per question)\n const shuffledOptions = useMemo(() => {\n return shuffleWithSeed(questionOptions, currentQuestion?.id || '');\n }, [questionOptions, currentQuestion?.id]);\n\n // Parse current answers\n // In result mode, read from fillAnswers object, fallback to answer JSON\n // In default mode, read from currentAnswer.answer JSON string\n const parsedAnswers: Record<string, string> = useMemo(() => {\n if (variant === QuizVariant.RESULT && currentQuestionResult?.fillAnswers) {\n // fillAnswers is already a Record<placeholderId, selectedOptionId>\n return currentQuestionResult.fillAnswers;\n }\n // Fallback to answer JSON string (for older/partially migrated submissions)\n return parseStoredAnswers(\n variant,\n currentQuestionResult?.answer,\n currentAnswer?.answer\n );\n }, [\n variant,\n currentQuestionResult?.fillAnswers,\n currentQuestionResult?.answer,\n currentAnswer?.answer,\n ]);\n\n const [localAnswers, setLocalAnswers] =\n useState<Record<string, string>>(parsedAnswers);\n\n // Sync local answers when question changes\n useEffect(() => {\n setLocalAnswers(parsedAnswers);\n }, [parsedAnswers, currentQuestion?.id]);\n\n // Handle select change\n const handleSelectChange = (placeholderId: string, optionId: string) => {\n if (!currentQuestion) return;\n\n // Get the LATEST answers directly from the store (not from closure)\n const latestStoreAnswer = getUserAnswerByQuestionId(currentQuestion.id);\n let baseAnswers: Record<string, string> = {};\n if (latestStoreAnswer?.answer) {\n try {\n const parsed = JSON.parse(latestStoreAnswer.answer);\n baseAnswers = normalizeAnswerData(parsed);\n } catch {\n // Invalid JSON, start fresh\n }\n }\n\n const newAnswers = {\n ...baseAnswers,\n ...localAnswers,\n [placeholderId]: optionId,\n };\n setLocalAnswers(newAnswers);\n\n // Save to store as JSON string\n selectDissertativeAnswer(currentQuestion.id, JSON.stringify(newAnswers));\n };\n\n // Get option text by ID\n const getOptionTextById = (optionId: string): string => {\n const option = questionOptions.find((opt) => opt.id === optionId);\n return stripHtmlTags(option?.option || '');\n };\n\n // Check if an answer is correct (for result mode)\n const isAnswerCorrect = (placeholderId: string): boolean => {\n const selectedOptionId = localAnswers[placeholderId];\n // The placeholder ID is the correct option ID\n return selectedOptionId === placeholderId;\n };\n\n // Render the select for default mode\n const renderDefaultSelect = (placeholderId: string) => {\n const selectedOptionId = localAnswers[placeholderId];\n\n return (\n <span\n key={placeholderId}\n className=\"inline-block align-middle mx-1 my-2\"\n style={{ display: 'inline-block', verticalAlign: 'middle' }}\n >\n <Select\n value={selectedOptionId || undefined}\n onValueChange={(value) => handleSelectChange(placeholderId, value)}\n >\n <SelectTrigger\n className=\"w-auto min-w-[120px] max-w-[200px] h-7 px-2 bg-background border-gray-300 overflow-hidden truncate\"\n title={stripHtmlTags(\n shuffledOptions.find((o) => o.id === selectedOptionId)?.option ||\n ''\n )}\n >\n <SelectValue placeholder=\"Selecione opção\" />\n </SelectTrigger>\n <SelectContent>\n {shuffledOptions.map((option) => (\n <SelectItem\n key={option.id}\n value={option.id}\n title={stripHtmlTags(option.option)}\n truncate\n >\n {stripHtmlTags(option.option)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </span>\n );\n };\n\n // Render the result badge\n const renderResultBadge = (placeholderId: string) => {\n const selectedOptionId = localAnswers[placeholderId];\n const selectedOptionText = getOptionTextById(selectedOptionId);\n const isCorrect = isAnswerCorrect(placeholderId);\n\n if (!selectedOptionId) {\n return (\n <span\n key={placeholderId}\n className=\"inline-block align-middle mx-1 my-2\"\n style={{ display: 'inline-block', verticalAlign: 'middle' }}\n >\n <Badge\n variant=\"solid\"\n action=\"error\"\n iconRight={<XCircleIcon aria-hidden=\"true\" />}\n size=\"large\"\n className=\"py-1 px-2\"\n >\n Não respondido\n </Badge>\n </span>\n );\n }\n\n return (\n <span\n key={placeholderId}\n className=\"inline-block align-middle mx-1 my-2\"\n style={{ display: 'inline-block', verticalAlign: 'middle' }}\n >\n <Badge\n variant=\"solid\"\n action={isCorrect ? 'success' : 'error'}\n iconRight={\n isCorrect ? (\n <CheckCircleIcon aria-hidden=\"true\" />\n ) : (\n <XCircleIcon aria-hidden=\"true\" />\n )\n }\n size=\"large\"\n className=\"py-1 px-2\"\n >\n <span className=\"text-text-900\">{selectedOptionText}</span>\n {/* O certo/errado só aparece pela cor e pelo ícone; o leitor de\n tela precisa do resultado em texto (WCAG 1.4.1). */}\n <Text as=\"span\" className=\"sr-only\">\n {isCorrect ? ' — correta' : ' — incorreta'}\n </Text>\n </Badge>\n </span>\n );\n };\n\n // Render the correct answer for resolution\n const renderResolutionAnswer = (placeholderId: string) => {\n // The placeholderId IS the correct option ID\n const correctOptionText = getOptionTextById(placeholderId);\n\n return (\n <span className=\"inline mx-1 text-success-600 font-semibold border-b-2 border-success-600\">\n {/* O destaque verde não chega ao leitor de tela: o prefixo diz que\n este é o gabarito da lacuna. */}\n <Text as=\"span\" className=\"sr-only\">\n Resposta correta:{' '}\n </Text>\n {correctOptionText}\n </span>\n );\n };\n\n // Parse text and render with selects\n const renderTextWithSelects = (text: string, isResolution?: boolean) => {\n const elements: Array<{ element: string | ReactNode; id: string }> = [];\n let lastIndex = 0;\n const nextId = () => elements.length;\n\n // Match {uuid} placeholders (UUID format or any alphanumeric with hyphens)\n const regex = /\\{([a-zA-Z0-9-]+)\\}/g;\n let match;\n\n while ((match = regex.exec(text)) !== null) {\n const [fullMatch, placeholderId] = match;\n const startIndex = match.index;\n\n // Add text before the placeholder\n if (startIndex > lastIndex) {\n elements.push({\n element: text.slice(lastIndex, startIndex),\n id: `${baseId}-text-${nextId()}`,\n });\n }\n\n // Add the appropriate element based on variant and mode\n if (isResolution) {\n elements.push({\n element: renderResolutionAnswer(placeholderId),\n id: `${baseId}-resolution-${nextId()}`,\n });\n } else if (variant === QuizVariant.DEFAULT) {\n elements.push({\n element: renderDefaultSelect(placeholderId),\n id: `${baseId}-select-${nextId()}`,\n });\n } else {\n elements.push({\n element: renderResultBadge(placeholderId),\n id: `${baseId}-result-${nextId()}`,\n });\n }\n\n lastIndex = match.index + fullMatch.length;\n }\n\n // Add remaining text\n if (lastIndex < text.length) {\n elements.push({\n element: text.slice(lastIndex),\n id: `${baseId}-text-${nextId()}`,\n });\n }\n\n return elements;\n };\n\n // Render HTML content with selects replacing placeholders\n const renderHtmlWithSelects = (\n htmlContent: string,\n isResolution?: boolean\n ) => {\n // For now, we'll strip HTML and render plain text with selects\n // This preserves the placeholder functionality while handling HTML content\n const textContent = stripHtmlTags(htmlContent);\n return renderTextWithSelects(textContent, isResolution);\n };\n\n if (!currentQuestion || !additionalContent) {\n return (\n <>\n <QuizSubTitle subTitle=\"Preenchimento\" />\n <QuizContainer className=\"h-auto pb-0\">\n <div className=\"space-y-6 px-4 h-auto\">\n <Text\n size=\"md\"\n color=\"text-text-600\"\n weight=\"normal\"\n className={cn(paddingBottom)}\n >\n Nenhum conteúdo disponível para esta questão.\n </Text>\n </div>\n </QuizContainer>\n </>\n );\n }\n\n return (\n <>\n <QuizSubTitle subTitle=\"Preencha as lacunas\" />\n\n <QuizContainer\n className={cn('', variant !== QuizVariant.RESULT && paddingBottom)}\n >\n <div className=\"px-4\">\n <Text\n as=\"div\"\n size=\"lg\"\n color=\"text-text-900\"\n weight=\"normal\"\n className=\"leading-[2.5] *:inline\"\n >\n {renderHtmlWithSelects(additionalContent).map((element) => (\n <Fragment key={element.id}>{element.element}</Fragment>\n ))}\n </Text>\n </div>\n </QuizContainer>\n\n {variant === QuizVariant.RESULT && (\n <>\n <QuizSubTitle subTitle=\"Resposta correta\" />\n\n <QuizContainer className={cn('', paddingBottom)}>\n <div className=\"px-4\">\n <Text\n as=\"div\"\n size=\"lg\"\n color=\"text-text-900\"\n weight=\"normal\"\n className=\"leading-[2.5] *:inline\"\n >\n {renderHtmlWithSelects(additionalContent, true).map(\n (element) => (\n <Fragment key={element.id}>{element.element}</Fragment>\n )\n )}\n </Text>\n </div>\n </QuizContainer>\n </>\n )}\n </>\n );\n};\n\nconst QuizImageQuestion = ({ paddingBottom }: QuizVariantInterface) => {\n const {\n variant,\n getCurrentQuestion,\n getCurrentAnswer,\n selectDissertativeAnswer,\n getQuestionResultByQuestionId,\n } = useQuizStore();\n\n const currentQuestion = getCurrentQuestion();\n const currentAnswer = getCurrentAnswer();\n const currentQuestionResult = getQuestionResultByQuestionId(\n currentQuestion?.id || ''\n );\n\n // Get image URL from additionalContent\n const imageUrl = currentQuestion?.additionalContent || '';\n\n // Parse correct coordinates from first option (stored as JSON: {\"x\": number, \"y\": number})\n const correctPositionRelative = useMemo(() => {\n if (!currentQuestion?.options || currentQuestion.options.length === 0) {\n return { x: 0.5, y: 0.5 }; // Default center\n }\n try {\n const coords = JSON.parse(currentQuestion.options[0].option);\n if (typeof coords.x === 'number' && typeof coords.y === 'number') {\n // Coordinates are stored as percentages (0-100), convert to relative (0-1)\n return { x: coords.x / 100, y: coords.y / 100 };\n }\n } catch {\n // Fall back to default\n }\n return { x: 0.5, y: 0.5 };\n }, [currentQuestion?.options]);\n\n // Calculate correctRadiusRelative automatically based on the circle dimensions\n const calculateCorrectRadiusRelative = (): number => {\n // The correct answer circle has width: 15% and height: 30%\n // We'll use the average of these as the radius\n const circleWidthRelative = 0.15; // 15%\n const circleHeightRelative = 0.3; // 30%\n\n // Calculate the average radius (half of the average of width and height)\n const averageRadius = (circleWidthRelative + circleHeightRelative) / 4;\n\n // Add a small tolerance for better user experience\n const tolerance = 0.02; // 2% tolerance\n\n return averageRadius + tolerance;\n };\n\n const correctRadiusRelative = calculateCorrectRadiusRelative();\n\n // Parse user's answer from stored JSON or result\n const storedUserAnswer = useMemo(() => {\n if (variant === QuizVariant.RESULT && currentQuestionResult?.answer) {\n try {\n const coords = JSON.parse(currentQuestionResult.answer);\n if (typeof coords.x === 'number' && typeof coords.y === 'number') {\n return { x: coords.x / 100, y: coords.y / 100 };\n }\n } catch {\n // Fall back to null\n }\n } else if (currentAnswer?.answer) {\n try {\n const coords = JSON.parse(currentAnswer.answer);\n if (typeof coords.x === 'number' && typeof coords.y === 'number') {\n return { x: coords.x / 100, y: coords.y / 100 };\n }\n } catch {\n // Fall back to null\n }\n }\n return null;\n }, [variant, currentQuestionResult?.answer, currentAnswer?.answer]);\n\n const [clickPositionRelative, setClickPositionRelative] = useState<{\n x: number;\n y: number;\n } | null>(storedUserAnswer);\n\n // Sync with stored answer when it changes\n useEffect(() => {\n setClickPositionRelative(storedUserAnswer);\n }, [storedUserAnswer]);\n\n // Helper function to safely convert click coordinates to relative coordinates\n const convertToRelativeCoordinates = (\n x: number,\n y: number,\n rect: DOMRect\n ): { x: number; y: number } => {\n // Guard against division by zero or extremely small dimensions\n const safeWidth = Math.max(rect.width, 0.001);\n const safeHeight = Math.max(rect.height, 0.001);\n\n // Convert to relative coordinates and clamp to [0, 1] range\n const xRelative = Math.max(0, Math.min(1, x / safeWidth));\n const yRelative = Math.max(0, Math.min(1, y / safeHeight));\n\n return { x: xRelative, y: yRelative };\n };\n\n const handleImageClick = (event: MouseEvent<HTMLButtonElement>) => {\n if (variant === QuizVariant.RESULT) return;\n\n const rect = event.currentTarget.getBoundingClientRect();\n const x = event.clientX - rect.left;\n const y = event.clientY - rect.top;\n\n // Use helper function for safe conversion\n const positionRelative = convertToRelativeCoordinates(x, y, rect);\n setClickPositionRelative(positionRelative);\n\n // Save the answer to the store (as percentages 0-100)\n if (currentQuestion) {\n const answerJson = JSON.stringify({\n x: Math.round(positionRelative.x * 100),\n y: Math.round(positionRelative.y * 100),\n });\n selectDissertativeAnswer(currentQuestion.id, answerJson);\n }\n };\n\n const handleKeyboardActivate = () => {\n if (variant === QuizVariant.RESULT) return;\n // Choose a deterministic position for keyboard activation; center is a reasonable default\n const centerPosition = { x: 0.5, y: 0.5 };\n setClickPositionRelative(centerPosition);\n\n // Save the answer to the store (as percentages 0-100)\n if (currentQuestion) {\n const answerJson = JSON.stringify({\n x: Math.round(centerPosition.x * 100),\n y: Math.round(centerPosition.y * 100),\n });\n selectDissertativeAnswer(currentQuestion.id, answerJson);\n }\n };\n\n const isCorrect = () => {\n if (!clickPositionRelative) return false;\n\n const distance = Math.sqrt(\n Math.pow(clickPositionRelative.x - correctPositionRelative.x, 2) +\n Math.pow(clickPositionRelative.y - correctPositionRelative.y, 2)\n );\n\n return distance <= correctRadiusRelative;\n };\n\n const getUserCircleColorClasses = () => {\n if (variant === QuizVariant.DEFAULT) {\n return 'bg-indicator-primary/70 border-[#F8CC2E]';\n }\n\n if (variant === QuizVariant.RESULT) {\n return isCorrect()\n ? 'bg-success-600/70 border-white' // Green for correct answer\n : 'bg-indicator-error/70 border-white'; // Red for incorrect answer\n }\n\n return 'bg-success-600/70 border-white';\n };\n\n // In review the question is read, not answered: the shared view draws the\n // image, the answer area and the student's click with the very tolerance the\n // backend graded with. This screen used to draw its own circle with a radius\n // of its own, so the drawing could disagree with the score.\n if (variant === QuizVariant.RESULT) {\n return (\n <QuizContainer className={cn('', paddingBottom)}>\n <div data-testid=\"quiz-image-container\" className=\"p-3\">\n <ImageAnswerView\n imageUrl={imageUrl}\n correctPoint={parseImageCorrectPoint(currentQuestion?.options)}\n studentPoint={parseImageStudentPoint(currentQuestionResult)}\n toleranceRadius={\n currentQuestionResult?.imageTolerance ?? DEFAULT_IMAGE_TOLERANCE\n }\n />\n </div>\n </QuizContainer>\n );\n }\n\n return (\n <>\n <QuizSubTitle subTitle=\"Clique na área correta\" />\n\n <QuizContainer className={cn('', paddingBottom)}>\n <div\n data-testid=\"quiz-image-container\"\n className=\"space-y-6 p-3 relative inline-block\"\n >\n <button\n data-testid=\"quiz-image-button\"\n type=\"button\"\n className=\"relative cursor-pointer w-full h-full border-0 bg-transparent p-0\"\n onClick={handleImageClick}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n handleKeyboardActivate();\n }\n }}\n aria-label=\"Área da imagem interativa\"\n >\n <img\n data-testid=\"quiz-image\"\n src={imageUrl || ImageQuestion}\n alt=\"Question\"\n className=\"w-full h-auto rounded-md\"\n />\n\n {/* User's answer circle */}\n {clickPositionRelative && (\n <div\n data-testid=\"quiz-user-circle\"\n className={`absolute rounded-full border-4 pointer-events-none ${getUserCircleColorClasses()}`}\n style={{\n minWidth: '30px',\n maxWidth: '52px',\n width: '5%',\n aspectRatio: '1 / 1',\n left: `calc(${clickPositionRelative.x * 100}% - 2.5%)`,\n top: `calc(${clickPositionRelative.y * 100}% - 2.5%)`,\n }}\n />\n )}\n </button>\n </div>\n </QuizContainer>\n </>\n );\n};\n\nexport {\n QuizSubTitle,\n QuizContainer,\n QuizMultipleChoice,\n QuizDissertative,\n QuizTrueOrFalse,\n QuizConnectDots,\n QuizFill,\n QuizImageQuestion,\n QuizAlternative,\n};\n","import type {\n Question,\n QuestionResult,\n} from '../../components/Quiz/useQuizStore';\nimport type { ImagePoint } from '../../components/shared/ImageAnswerView';\n\n/**\n * Fallback radius, in image percentage points, used when the payload does not\n * carry the backend's tolerance.\n *\n * The backend is the authority — it grades with its own value and now sends it\n * as `imageTolerance`. This only covers older payloads, and matches the value\n * the backend has always used.\n */\nexport const DEFAULT_IMAGE_TOLERANCE = 10;\n\n/**\n * Read the answer key point of an IMAGEM question.\n *\n * The key lives in the question's single option, whose text is the JSON\n * `{\"x\": number, \"y\": number}` in percentage points of the image.\n *\n * @param options - The question's options\n * @returns The correct point, or null when there is no usable key\n *\n * @example\n * ```typescript\n * parseImageCorrectPoint([{ id: 'o1', option: '{\"x\":50,\"y\":30}' }]); // { x: 50, y: 30 }\n * ```\n */\nexport const parseImageCorrectPoint = (\n options: Question['options'] | undefined\n): ImagePoint | null => {\n const first = options?.[0];\n if (!first?.option) {\n return null;\n }\n\n try {\n const parsed = JSON.parse(first.option);\n if (typeof parsed?.x === 'number' && typeof parsed?.y === 'number') {\n return { x: parsed.x, y: parsed.y };\n }\n } catch {\n return null;\n }\n\n return null;\n};\n\n/**\n * Read where the student clicked on an IMAGEM question.\n *\n * Prefers the structured `imageAnswer` the backend sends. Falls back to parsing\n * `answer`, which older payloads and the draft flow still use, in either the\n * `{x, y}` or `{coordinateX, coordinateY}` shape.\n *\n * @param result - The student's answer for the question\n * @returns The clicked point in percentage points (0-100), or null\n *\n * @example\n * ```typescript\n * parseImageStudentPoint({ imageAnswer: { coordinateX: 52, coordinateY: 28 } }); // { x: 52, y: 28 }\n * ```\n */\nexport const parseImageStudentPoint = (\n result: QuestionResult['answers'][number] | null | undefined\n): ImagePoint | null => {\n if (!result) {\n return null;\n }\n\n if (result.imageAnswer) {\n const { coordinateX, coordinateY } = result.imageAnswer;\n if (typeof coordinateX === 'number' && typeof coordinateY === 'number') {\n return { x: coordinateX, y: coordinateY };\n }\n }\n\n if (!result.answer) {\n return null;\n }\n\n try {\n const parsed = JSON.parse(result.answer);\n const x =\n typeof parsed?.coordinateX === 'number' ? parsed.coordinateX : parsed?.x;\n const y =\n typeof parsed?.coordinateY === 'number' ? parsed.coordinateY : parsed?.y;\n if (typeof x === 'number' && typeof y === 'number') {\n return { x, y };\n }\n } catch {\n return null;\n }\n\n return null;\n};\n"]}
1
+ {"version":3,"sources":["/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/chunk-72D7G6YQ.js","../src/components/shared/ImageAnswerView.tsx","../src/components/Quiz/QuizContent.tsx","../src/utils/image/imageAnswer.utils.ts"],"names":["jsx","Fragment","jsxs"],"mappings":"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","file":"/home/runner/work/analytica-frontend-lib/analytica-frontend-lib/dist/chunk-72D7G6YQ.js","sourcesContent":[null,"import Text from '../Text/Text';\nimport { cn } from '../../utils/utils';\n\n/** A point on the image, in percentage points of its width/height (0-100). */\nexport interface ImagePoint {\n x: number;\n y: number;\n}\n\nexport interface ImageAnswerViewProps {\n /** URL of the image the student had to click on */\n readonly imageUrl: string;\n /** The answer key point. Null when the question carries no key. */\n readonly correctPoint: ImagePoint | null;\n /** Where the student clicked. Null when the question was left blank. */\n readonly studentPoint: ImagePoint | null;\n /**\n * Radius within which a click counts as correct, in the same percentage\n * space. Comes from the backend so the drawn circle matches the grade.\n */\n readonly toleranceRadius: number;\n /** Whether the answer key may be revealed (false while pending) */\n readonly showCorrectness?: boolean;\n}\n\n/** Describe a point for screen readers. */\nconst describePoint = (point: ImagePoint): string =>\n `${Math.round(point.x)}% da esquerda, ${Math.round(point.y)}% do topo`;\n\n/** Euclidean distance between two points in the 0-100 space. */\nconst distanceBetween = (a: ImagePoint, b: ImagePoint): number =>\n Math.hypot(a.x - b.x, a.y - b.y);\n\n/**\n * Read-only view of an image question: the image, the answer key area and where\n * the student clicked.\n *\n * Shared by the activity correction modal, the simulation detail modal and the\n * student's result screen, which used to carry three divergent copies of this —\n * one of them drawing a mock image with a hardcoded answer area, and each one\n * deciding correctness with a different radius of its own. Here there is a\n * single rule, and it is the backend's: a click is correct when its distance to\n * the correct point is at most `toleranceRadius`.\n *\n * The target circle is drawn with the diameter of that tolerance, so what the\n * reader sees is what decided the grade.\n *\n * @param props - Component props\n * @returns JSX element\n *\n * @example\n * ```tsx\n * <ImageAnswerView\n * imageUrl={question.additionalContent ?? ''}\n * correctPoint={{ x: 50, y: 30 }}\n * studentPoint={{ x: 52, y: 28 }}\n * toleranceRadius={10}\n * />\n * ```\n */\nexport const ImageAnswerView = ({\n imageUrl,\n correctPoint,\n studentPoint,\n toleranceRadius,\n showCorrectness = true,\n}: ImageAnswerViewProps) => {\n const canShowCorrectness = showCorrectness && correctPoint !== null;\n const isCorrect =\n correctPoint !== null &&\n studentPoint !== null &&\n distanceBetween(studentPoint, correctPoint) <= toleranceRadius;\n\n /** Full spoken summary of the question, the only non-visual access to it. */\n const spatialSummary = (): string => {\n if (!correctPoint) {\n return studentPoint\n ? `Resposta do aluno em ${describePoint(studentPoint)}.`\n : 'Nenhuma resposta do aluno fornecida.';\n }\n\n const correctDescription = describePoint(correctPoint);\n if (!studentPoint) {\n return `Área correta localizada em ${correctDescription}. Nenhuma resposta do aluno fornecida.`;\n }\n\n const deltaX = studentPoint.x - correctPoint.x;\n const deltaY = studentPoint.y - correctPoint.y;\n let direction: string;\n if (Math.abs(deltaX) > Math.abs(deltaY)) {\n direction = deltaX > 0 ? 'à direita' : 'à esquerda';\n } else {\n direction = deltaY > 0 ? 'abaixo' : 'acima';\n }\n\n const distance = Math.round(distanceBetween(studentPoint, correctPoint));\n const verdict = isCorrect\n ? 'A resposta está dentro da área de tolerância e é considerada correta.'\n : 'A resposta está fora da área de tolerância e é considerada incorreta.';\n\n return `Área correta localizada em ${correctDescription}. Resposta do aluno em ${describePoint(studentPoint)}. A resposta do aluno está ${distance}% de distância ${direction} da área correta. ${verdict}`;\n };\n\n return (\n <div className=\"pt-2 space-y-4\">\n <div className=\"flex items-center gap-4 text-xs\">\n {canShowCorrectness && (\n <div className=\"flex items-center gap-2\">\n <div className=\"w-3 h-3 rounded-full bg-indicator-primary/70 border border-[#F8CC2E]\" />\n <Text size=\"sm\" weight=\"normal\" color=\"text-text-600\">\n Área correta\n </Text>\n </div>\n )}\n {canShowCorrectness && studentPoint && (\n <div className=\"flex items-center gap-2\">\n <div\n className={cn(\n 'w-3 h-3 rounded-full border border-white',\n isCorrect ? 'bg-success-600/70' : 'bg-indicator-error/70'\n )}\n />\n <Text size=\"sm\" weight=\"normal\" color=\"text-text-600\">\n {isCorrect ? 'Resposta correta' : 'Resposta incorreta'}\n </Text>\n </div>\n )}\n </div>\n\n <div className=\"relative w-full\">\n <div className=\"sr-only\">{spatialSummary()}</div>\n\n {imageUrl ? (\n <img\n src={imageUrl}\n alt={\n correctPoint\n ? `Questão de imagem com área correta localizada em ${describePoint(correctPoint)}`\n : 'Questão de imagem'\n }\n className=\"w-full h-auto rounded-md\"\n />\n ) : (\n <div className=\"rounded-md border border-border-100 bg-background-50 p-6 text-center\">\n <Text size=\"sm\" className=\"text-text-600\">\n Imagem da questão indisponível\n </Text>\n </div>\n )}\n\n {canShowCorrectness && correctPoint && (\n <div\n aria-hidden=\"true\"\n data-testid=\"image-correct-area\"\n className=\"absolute rounded-full bg-indicator-primary/70 border-4 border-[#F8CC2E] pointer-events-none -translate-x-1/2 -translate-y-1/2\"\n style={{\n // Diameter = twice the tolerance, so the circle IS the pass mark.\n width: `${toleranceRadius * 2}%`,\n aspectRatio: '1 / 1',\n left: `${correctPoint.x}%`,\n top: `${correctPoint.y}%`,\n }}\n />\n )}\n\n {studentPoint && (\n <div\n aria-hidden=\"true\"\n data-testid=\"image-student-point\"\n className={cn(\n 'absolute rounded-full border-4 pointer-events-none -translate-x-1/2 -translate-y-1/2',\n canShowCorrectness &&\n isCorrect &&\n 'bg-success-600/70 border-white',\n canShowCorrectness &&\n !isCorrect &&\n 'bg-indicator-error/70 border-white',\n !canShowCorrectness && 'bg-indicator-primary/70 border-[#F8CC2E]'\n )}\n style={{\n minWidth: '20px',\n maxWidth: '40px',\n width: '4%',\n aspectRatio: '1 / 1',\n left: `${studentPoint.x}%`,\n top: `${studentPoint.y}%`,\n }}\n />\n )}\n </div>\n </div>\n );\n};\n\nexport default ImageAnswerView;\n","import {\n Fragment,\n forwardRef,\n MouseEvent,\n ReactNode,\n useCallback,\n useEffect,\n useId,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport { ANSWER_STATUS, useQuizStore } from './useQuizStore';\nimport { QuizVariant } from './Quiz.types';\nimport CorrectionSourceTag from './CorrectionSourceTag';\nimport { TrueFalseEnum } from '../../enums/Quiz';\nimport {\n prependLetterToHtml,\n getTrueOrFalseOptionState,\n getQuestionA11yIds,\n shuffleWithSeed,\n} from './Quiz.utils';\nimport { cn } from '../../utils/utils';\nimport { stripHtmlTags } from '../../utils/stringUtils';\nimport Select, {\n SelectContent,\n SelectItem,\n SelectTrigger,\n SelectValue,\n} from '../Select/Select';\nimport TextArea from '../TextArea/TextArea';\nimport { AlternativesList } from '../Alternative/Alternative';\nimport { OptionStatus } from '../../enums/Options';\nimport { MultipleChoiceList } from '../MultipleChoice/MultipleChoice';\nimport Badge from '../Badge/Badge';\nimport { CheckCircleIcon } from '@phosphor-icons/react/dist/csr/CheckCircle';\nimport { XCircleIcon } from '@phosphor-icons/react/dist/csr/XCircle';\nimport ImageQuestion from '../../assets/img/mock-image-question.png';\nimport { ImageAnswerView } from '../shared/ImageAnswerView';\nimport {\n DEFAULT_IMAGE_TOLERANCE,\n parseImageCorrectPoint,\n parseImageStudentPoint,\n} from '../../utils/image/imageAnswer.utils';\nimport Text from '../Text/Text';\nimport { HtmlMathRenderer } from '../HtmlMathRenderer';\nexport const getStatusBadge = (status?: OptionStatus) => {\n switch (status) {\n case OptionStatus.CORRECT:\n return (\n <Badge\n variant=\"solid\"\n action=\"success\"\n iconLeft={<CheckCircleIcon aria-hidden=\"true\" />}\n >\n Resposta correta\n </Badge>\n );\n case OptionStatus.INCORRECT:\n return (\n <Badge\n variant=\"solid\"\n action=\"error\"\n iconLeft={<XCircleIcon aria-hidden=\"true\" />}\n >\n Resposta incorreta\n </Badge>\n );\n default:\n return null;\n }\n};\n\nexport const getStatusStyles = (variantCorrect?: OptionStatus) => {\n switch (variantCorrect) {\n case OptionStatus.CORRECT:\n return 'bg-success-background border-success-300';\n case OptionStatus.INCORRECT:\n return 'bg-error-background border-error-300';\n default:\n return '';\n }\n};\n\n/**\n * Normalizes parsed answer data to Record<string, string> format.\n * Handles legacy array format [{ optionId, selectedValue }] by converting to { [optionId]: selectedValue }.\n */\nconst normalizeAnswerData = (parsed: unknown): Record<string, string> => {\n // Handle legacy array format: [{ optionId: string, selectedValue: string }]\n if (Array.isArray(parsed)) {\n const result: Record<string, string> = {};\n for (const item of parsed) {\n if (\n item &&\n typeof item === 'object' &&\n 'optionId' in item &&\n 'selectedValue' in item\n ) {\n result[item.optionId as string] = item.selectedValue as string;\n }\n }\n return result;\n }\n\n // Handle expected object format: { [optionId]: selectedValue }\n if (parsed && typeof parsed === 'object') {\n return parsed as Record<string, string>;\n }\n\n return {};\n};\n\n/**\n * Parses JSON answers from stored answer string.\n * In result mode, uses persisted results. Otherwise, uses current draft answers.\n * Normalizes legacy array format to expected Record<string, string> map.\n */\nconst parseStoredAnswers = (\n variant: QuizVariant,\n resultAnswer: string | null | undefined,\n currentAnswer: string | null | undefined\n): Record<string, string> => {\n if (variant === QuizVariant.RESULT) {\n if (!resultAnswer) return {};\n try {\n const parsed = JSON.parse(resultAnswer);\n return normalizeAnswerData(parsed);\n } catch {\n return {};\n }\n }\n if (currentAnswer) {\n try {\n const parsed = JSON.parse(currentAnswer);\n return normalizeAnswerData(parsed);\n } catch {\n return {};\n }\n }\n return {};\n};\n\n/**\n * Converts isCorrect boolean to status variant string.\n * Returns undefined for null/unanswered to keep neutral styling.\n */\nconst getAnswerStatus = (\n isCorrect: boolean | null\n): OptionStatus | undefined => {\n if (isCorrect === true) return OptionStatus.CORRECT;\n if (isCorrect === false) return OptionStatus.INCORRECT;\n return undefined;\n};\n\ninterface QuizVariantInterface {\n paddingBottom?: string;\n}\n\nconst QuizSubTitle = forwardRef<HTMLDivElement, { subTitle: string }>(\n ({ subTitle, ...props }, ref) => {\n return (\n <div className=\"px-4 pb-2 pt-6\" {...props} ref={ref}>\n {/* Subtítulo de seção da questão (ex.: \"Alternativas\"): h3 para\n entrar na navegação por cabeçalhos abaixo dos h1/h2 da tela. */}\n <Text as=\"h3\" size=\"lg\" weight=\"bold\" color=\"text-text-950\">\n {subTitle}\n </Text>\n </div>\n );\n }\n);\n\nconst QuizContainer = forwardRef<\n HTMLDivElement,\n { children: ReactNode; className?: string }\n>(({ children, className, ...props }, ref) => {\n return (\n <div\n ref={ref}\n className={cn(\n 'bg-background rounded-t-xl px-4 pt-4 pb-[80px] h-auto flex flex-col gap-4 mb-auto',\n className\n )}\n {...props}\n >\n {children}\n </div>\n );\n});\n\nconst QuizAlternative = ({ paddingBottom }: QuizVariantInterface) => {\n const {\n getCurrentQuestion,\n selectAnswer,\n getQuestionResultByQuestionId,\n getCurrentAnswer,\n variant,\n } = useQuizStore();\n const currentQuestion = getCurrentQuestion();\n const a11yIds = getQuestionA11yIds(currentQuestion?.id);\n const currentQuestionResult = getQuestionResultByQuestionId(\n currentQuestion?.id || ''\n );\n\n const currentAnswer = getCurrentAnswer();\n const alternatives = currentQuestion?.options?.map((option) => {\n let status: OptionStatus = OptionStatus.NEUTRAL;\n if (variant === QuizVariant.RESULT) {\n const isCorrectOption =\n currentQuestionResult?.options?.find((op) => op.id === option.id)\n ?.isCorrect || false;\n\n const isSelected = currentQuestionResult?.selectedOptions.some(\n (selectedOption) => selectedOption.optionId === option.id\n );\n\n // Only show correct/incorrect status if answer is not pending evaluation\n const shouldShowCorrectAnswers =\n currentQuestionResult?.answerStatus !==\n ANSWER_STATUS.PENDENTE_AVALIACAO &&\n currentQuestionResult?.answerStatus !== ANSWER_STATUS.NAO_RESPONDIDO;\n\n if (shouldShowCorrectAnswers) {\n if (isCorrectOption) {\n status = OptionStatus.CORRECT;\n } else if (isSelected && !isCorrectOption) {\n status = OptionStatus.INCORRECT;\n } else {\n status = OptionStatus.NEUTRAL;\n }\n } else {\n // When pending evaluation, show all options as neutral\n status = OptionStatus.NEUTRAL;\n }\n }\n\n return {\n label: option.option,\n value: option.id,\n status: status,\n };\n });\n\n if (!alternatives)\n return (\n <div>\n <p>Não há Alternativas</p>\n </div>\n );\n\n return (\n <>\n <QuizSubTitle subTitle=\"Alternativas\" />\n\n <QuizContainer className={cn('', paddingBottom)}>\n <div className=\"space-y-4\">\n <AlternativesList\n labelledBy={a11yIds.titleId}\n describedBy={a11yIds.statementId}\n mode={variant === QuizVariant.DEFAULT ? 'interactive' : 'readonly'}\n key={`question-${currentQuestion?.id || '1'}`}\n name={`question-${currentQuestion?.id || '1'}`}\n layout=\"compact\"\n alternatives={alternatives}\n value={\n variant === QuizVariant.RESULT\n ? currentQuestionResult?.selectedOptions[0]?.optionId || ''\n : currentAnswer?.optionId || ''\n }\n selectedValue={\n variant === QuizVariant.RESULT\n ? currentQuestionResult?.selectedOptions[0]?.optionId || ''\n : currentAnswer?.optionId || ''\n }\n onValueChange={(value) => {\n if (currentQuestion) {\n selectAnswer(currentQuestion.id, value);\n }\n }}\n />\n </div>\n </QuizContainer>\n </>\n );\n};\n\nconst QuizMultipleChoice = ({ paddingBottom }: QuizVariantInterface) => {\n const {\n getCurrentQuestion,\n selectMultipleAnswer,\n getQuestionResultByQuestionId,\n variant,\n } = useQuizStore();\n const currentQuestion = getCurrentQuestion();\n const a11yIds = getQuestionA11yIds(currentQuestion?.id);\n // Derive the current question's answers from the stable `userAnswers` slice\n // instead of calling getAllCurrentAnswer(), which allocates a fresh array on\n // every render. That fresh identity propagated through the memos below and,\n // combined with the store→prop→local-state mirror in MultipleChoiceList,\n // caused a \"Maximum update depth exceeded\" loop on /questionario.\n const userAnswers = useQuizStore((state) => state.userAnswers);\n const allCurrentAnswers = useMemo(\n () =>\n currentQuestion\n ? userAnswers.filter(\n (answer) => answer.questionId === currentQuestion.id\n )\n : [],\n [userAnswers, currentQuestion]\n );\n const currentQuestionResult = getQuestionResultByQuestionId(\n currentQuestion?.id || ''\n );\n // Use ref to track previous values and prevent unnecessary updates\n const prevSelectedValuesRef = useRef<string[]>([]);\n const prevQuestionIdRef = useRef<string>('');\n\n // Memoize the answer IDs to prevent unnecessary re-renders\n // For MULTIPLA_ESCOLHA, we now have a single answer entry with selectedOptionIds array\n const allCurrentAnswerIds = useMemo(() => {\n if (allCurrentAnswers && allCurrentAnswers.length > 0) {\n // Prefer any row already using the new format\n const answerWithSelectedOptionIds = allCurrentAnswers.find((answer) =>\n Array.isArray(answer.selectedOptionIds)\n );\n if (answerWithSelectedOptionIds) {\n return answerWithSelectedOptionIds.selectedOptionIds ?? [];\n }\n // Fallback to old format (multiple entries with optionId)\n return allCurrentAnswers.map((answer) => answer.optionId);\n }\n return [];\n }, [allCurrentAnswers]);\n\n // Memoize the selected values to prevent infinite loops\n const selectedValues = useMemo(() => {\n return allCurrentAnswerIds?.filter((id): id is string => id !== null) || [];\n }, [allCurrentAnswerIds]);\n\n // Only update selectedValues if they actually changed or question changed\n const stableSelectedValues = useMemo(() => {\n // In result mode, always use selectedOptions from the question result\n if (variant === QuizVariant.RESULT) {\n return (\n currentQuestionResult?.selectedOptions?.map((op) => op.optionId) || []\n );\n }\n\n const currentQuestionId = currentQuestion?.id || '';\n const hasQuestionChanged = prevQuestionIdRef.current !== currentQuestionId;\n\n if (hasQuestionChanged) {\n prevQuestionIdRef.current = currentQuestionId;\n prevSelectedValuesRef.current = selectedValues;\n return selectedValues;\n }\n\n // Only update if values actually changed\n const hasValuesChanged =\n JSON.stringify(prevSelectedValuesRef.current) !==\n JSON.stringify(selectedValues);\n if (hasValuesChanged) {\n prevSelectedValuesRef.current = selectedValues;\n return selectedValues;\n }\n\n return prevSelectedValuesRef.current;\n }, [\n selectedValues,\n currentQuestion?.id,\n variant,\n currentQuestionResult?.selectedOptions,\n ]);\n\n // Memoize the callback to prevent unnecessary re-renders\n const handleSelectedValues = useCallback(\n (values: string[]) => {\n if (currentQuestion) {\n selectMultipleAnswer(currentQuestion.id, values);\n }\n },\n [currentQuestion, selectMultipleAnswer]\n );\n\n // Create a stable key to force re-mount when question changes\n const questionKey = useMemo(\n () => `question-${currentQuestion?.id || '1'}`,\n [currentQuestion?.id]\n );\n const choices = currentQuestion?.options?.map((option) => {\n let status: OptionStatus = OptionStatus.NEUTRAL;\n\n if (variant === QuizVariant.RESULT) {\n const isCorrectOption =\n currentQuestionResult?.options?.find((op) => op.id === option.id)\n ?.isCorrect || false;\n\n const isSelected = currentQuestionResult?.selectedOptions?.some(\n (op) => op.optionId === option.id\n );\n\n // Only show correct/incorrect status if answer is not pending evaluation\n const shouldShowCorrectAnswers =\n currentQuestionResult?.answerStatus !==\n ANSWER_STATUS.PENDENTE_AVALIACAO &&\n currentQuestionResult?.answerStatus !== ANSWER_STATUS.NAO_RESPONDIDO;\n\n if (shouldShowCorrectAnswers) {\n if (isCorrectOption) {\n status = OptionStatus.CORRECT;\n } else if (isSelected && !isCorrectOption) {\n status = OptionStatus.INCORRECT;\n } else {\n status = OptionStatus.NEUTRAL;\n }\n } else {\n // When pending evaluation, show all options as neutral\n status = OptionStatus.NEUTRAL;\n }\n }\n\n return {\n label: option.option,\n value: option.id,\n status: status,\n };\n });\n\n if (!choices)\n return (\n <div>\n <p>Não há Escolhas Multiplas</p>\n </div>\n );\n return (\n <>\n <QuizSubTitle subTitle=\"Alternativas\" />\n\n <QuizContainer className={cn('', paddingBottom)}>\n <div className=\"space-y-4\">\n <MultipleChoiceList\n labelledBy={a11yIds.titleId}\n describedBy={a11yIds.statementId}\n choices={choices}\n key={questionKey}\n name={questionKey}\n selectedValues={stableSelectedValues}\n onHandleSelectedValues={handleSelectedValues}\n mode={variant === QuizVariant.DEFAULT ? 'interactive' : 'readonly'}\n />\n </div>\n </QuizContainer>\n </>\n );\n};\n\nconst QuizDissertative = ({ paddingBottom }: QuizVariantInterface) => {\n const {\n getCurrentQuestion,\n getCurrentAnswer,\n selectDissertativeAnswer,\n getQuestionResultByQuestionId,\n variant,\n getDissertativeCharLimit,\n } = useQuizStore();\n\n const currentQuestion = getCurrentQuestion();\n const a11yIds = getQuestionA11yIds(currentQuestion?.id);\n const currentQuestionResult = getQuestionResultByQuestionId(\n currentQuestion?.id || ''\n );\n\n const currentAnswer = getCurrentAnswer();\n const textareaRef = useRef<HTMLTextAreaElement>(null);\n const charLimit = getDissertativeCharLimit();\n\n const handleAnswerChange = (value: string) => {\n if (currentQuestion) {\n selectDissertativeAnswer(currentQuestion.id, value);\n }\n };\n\n // Auto-resize function\n const adjustTextareaHeight = useCallback(() => {\n if (textareaRef.current) {\n textareaRef.current.style.height = 'auto';\n const scrollHeight = textareaRef.current.scrollHeight;\n const minHeight = 120; // 120px minimum height\n const maxHeight = 400; // 400px maximum height\n const newHeight = Math.min(Math.max(scrollHeight, minHeight), maxHeight);\n textareaRef.current.style.height = `${newHeight}px`;\n }\n }, []);\n\n // Adjust height when currentAnswer changes\n useEffect(() => {\n adjustTextareaHeight();\n }, [currentAnswer, adjustTextareaHeight]);\n\n if (!currentQuestion) {\n return (\n <div className=\"space-y-4\">\n <p className=\"text-text-600 text-md\">Nenhuma questão disponível</p>\n </div>\n );\n }\n\n const localAnswer =\n (variant == 'result'\n ? currentQuestionResult?.answer\n : currentAnswer?.answer) || '';\n return (\n <>\n <div className=\"flex items-center justify-between gap-2 flex-wrap\">\n <QuizSubTitle subTitle=\"Resposta\" />\n {/* Quem corrigiu esta dissertativa. Fica aqui, ao lado da resposta, e\n não só dentro do modal de feedback: o aluno tem que saber que a\n correção veio da IA sem precisar abrir nada. Só aparece no\n resultado — durante a prova não há correção nenhuma. */}\n {variant === QuizVariant.RESULT && (\n <CorrectionSourceTag\n correctionSource={currentQuestionResult?.correctionSource}\n aiCorrectionStatus={currentQuestionResult?.aiCorrectionStatus}\n />\n )}\n </div>\n\n <QuizContainer className={cn(variant != 'result' && paddingBottom)}>\n <div className=\"space-y-4 max-h-[600px] overflow-y-auto\">\n {variant === QuizVariant.DEFAULT ? (\n <div className=\"space-y-4\">\n <TextArea\n ref={textareaRef}\n aria-labelledby={a11yIds.titleId}\n aria-describedby={a11yIds.statementId}\n placeholder=\"Escreva sua resposta\"\n value={localAnswer}\n onChange={(e) => handleAnswerChange(e.target.value)}\n rows={4}\n className=\"min-h-[120px] max-h-[400px] resize-none overflow-y-auto\"\n maxLength={charLimit}\n showCharacterCount={!!charLimit}\n />\n </div>\n ) : (\n <div className=\"space-y-4\">\n <p className=\"text-text-600 text-md whitespace-pre-wrap\">\n {localAnswer || 'Nenhuma resposta fornecida'}\n </p>\n </div>\n )}\n </div>\n </QuizContainer>\n\n {/* The teacher's comment is rendered by `TeacherQuestionComment`, mounted\n once in `QuizContent` for every question type. This block used to\n duplicate it here, so an essay showed the same text twice. */}\n </>\n );\n};\n\nconst QuizTrueOrFalse = ({ paddingBottom }: QuizVariantInterface) => {\n const {\n variant,\n getCurrentQuestion,\n getCurrentAnswer,\n selectDissertativeAnswer,\n getQuestionResultByQuestionId,\n getUserAnswerByQuestionId,\n } = useQuizStore();\n\n const currentQuestion = getCurrentQuestion();\n const currentAnswer = getCurrentAnswer();\n const currentQuestionResult = getQuestionResultByQuestionId(\n currentQuestion?.id || ''\n );\n\n // Get the options - prefer persisted result options in result mode, fallback to live question\n const options = useMemo(() => {\n if (variant === QuizVariant.RESULT && currentQuestionResult?.options) {\n return currentQuestionResult.options;\n }\n return currentQuestion?.options || [];\n }, [variant, currentQuestionResult?.options, currentQuestion?.options]);\n\n // Parse stored answers from JSON string, handling both object and legacy array formats\n const parsedAnswers: Record<string, string> = useMemo(() => {\n return parseStoredAnswers(\n variant,\n currentQuestionResult?.answer,\n currentAnswer?.answer\n );\n }, [variant, currentQuestionResult?.answer, currentAnswer?.answer]);\n\n // Local state to track answers: { \"optionId\": \"V\" | \"F\" }\n const [localAnswers, setLocalAnswers] =\n useState<Record<string, string>>(parsedAnswers);\n\n // Sync local answers when question changes or parsed answers update\n useEffect(() => {\n setLocalAnswers(parsedAnswers);\n }, [parsedAnswers, currentQuestion?.id]);\n\n // Handle select change for an option\n const handleSelectChange = (optionId: string, value: string) => {\n if (!currentQuestion) return;\n\n // Get the LATEST answers directly from the store (not from closure)\n // This is critical to avoid stale closure issues when user selects options quickly\n const latestStoreAnswer = getUserAnswerByQuestionId(currentQuestion.id);\n let baseAnswers: Record<string, string> = {};\n if (latestStoreAnswer?.answer) {\n try {\n const parsed = JSON.parse(latestStoreAnswer.answer);\n baseAnswers = normalizeAnswerData(parsed);\n } catch {\n // Invalid JSON, start fresh\n }\n }\n\n // Merge: store answers + local state + new selection\n const mergedAnswers = {\n ...baseAnswers,\n ...localAnswers,\n [optionId]: value,\n };\n setLocalAnswers(mergedAnswers);\n\n // Save to store as JSON string\n selectDissertativeAnswer(currentQuestion.id, JSON.stringify(mergedAnswers));\n };\n\n // Get the current selection for an option\n const getOptionSelection = (optionId: string): string | undefined => {\n return localAnswers[optionId];\n };\n\n const getLetterByIndex = (index: number) => String.fromCodePoint(97 + index); // 97 = 'a' in ASCII\n\n const isDefaultVariant = variant === QuizVariant.DEFAULT;\n\n // Check if we should show correct/incorrect status\n const shouldShowStatus =\n currentQuestionResult?.answerStatus !== ANSWER_STATUS.PENDENTE_AVALIACAO &&\n currentQuestionResult?.answerStatus !== ANSWER_STATUS.NAO_RESPONDIDO;\n\n if (options.length === 0) {\n return (\n <QuizContainer className={cn('', paddingBottom)}>\n <Text size=\"sm\" className=\"text-text-500 italic\">\n Nenhuma opção disponível para esta questão\n </Text>\n </QuizContainer>\n );\n }\n\n return (\n <>\n <QuizSubTitle subTitle=\"Alternativas\" />\n\n <QuizContainer className={cn('', paddingBottom)}>\n <div className=\"flex flex-col gap-3.5\">\n {options.map((option, index) => {\n // Use helper to compute all option state\n const {\n hasAnswered,\n studentAnswer,\n correctAnswer,\n isStudentCorrect,\n variantCorrect,\n } = getTrueOrFalseOptionState(\n option.id,\n variant,\n currentQuestionResult,\n localAnswers\n );\n\n const contentWithLetter = prependLetterToHtml(\n getLetterByIndex(index),\n option.option\n );\n\n return (\n <section\n key={option.id || `option-${index}`}\n className=\"flex flex-col gap-2\"\n >\n <div\n className={cn(\n 'w-full grid grid-cols-[1fr_auto] items-start gap-4 p-2 rounded-md border',\n !isDefaultVariant && shouldShowStatus && hasAnswered\n ? getStatusStyles(variantCorrect)\n : 'border-transparent'\n )}\n >\n <HtmlMathRenderer\n content={contentWithLetter}\n className=\"text-text-900 text-sm\"\n />\n\n {isDefaultVariant ? (\n <Select\n size=\"medium\"\n value={getOptionSelection(option.id)}\n onValueChange={(value) =>\n handleSelectChange(option.id, value)\n }\n >\n <SelectTrigger className=\"w-[120px]\">\n <SelectValue placeholder=\"Selecione\" />\n </SelectTrigger>\n\n <SelectContent>\n <SelectItem value={TrueFalseEnum.VERDADEIRO}>\n Verdadeiro\n </SelectItem>\n <SelectItem value={TrueFalseEnum.FALSO}>\n Falso\n </SelectItem>\n </SelectContent>\n </Select>\n ) : (\n shouldShowStatus &&\n hasAnswered && <div>{getStatusBadge(variantCorrect)}</div>\n )}\n </div>\n\n {!isDefaultVariant && shouldShowStatus && (\n <span className=\"flex flex-row gap-2 items-center flex-wrap\">\n {hasAnswered ? (\n <>\n <Text size=\"2xs\" className=\"text-text-800\">\n Resposta selecionada: {studentAnswer}\n </Text>\n {!isStudentCorrect && (\n <Text size=\"2xs\" className=\"text-text-800\">\n {/* Separador só visual: o leitor leria \"barra vertical\". */}\n <Text\n as=\"span\"\n size=\"2xs\"\n color=\"text-text-800\"\n aria-hidden=\"true\"\n >\n |{' '}\n </Text>\n Resposta correta: {correctAnswer}\n </Text>\n )}\n </>\n ) : (\n <Text size=\"2xs\" className=\"text-text-800\">\n Não respondida{' '}\n <Text\n as=\"span\"\n size=\"2xs\"\n color=\"text-text-800\"\n aria-hidden=\"true\"\n >\n |\n </Text>{' '}\n Resposta correta: {correctAnswer}\n </Text>\n )}\n </span>\n )}\n </section>\n );\n })}\n </div>\n </QuizContainer>\n </>\n );\n};\n\ninterface UserAnswer {\n option: string;\n dotOption: string | null;\n correctOption: string;\n isCorrect: boolean | null;\n}\n\nconst QuizConnectDots = ({ paddingBottom }: QuizVariantInterface) => {\n const {\n variant,\n getCurrentQuestion,\n getCurrentAnswer,\n selectDissertativeAnswer,\n getQuestionResultByQuestionId,\n getUserAnswerByQuestionId,\n } = useQuizStore();\n\n const currentQuestion = getCurrentQuestion();\n const currentQuestionResult = getQuestionResultByQuestionId(\n currentQuestion?.id || ''\n );\n const currentAnswer = getCurrentAnswer();\n\n // Extract options from the current question\n // Each option has: id, option (left column), correctValue (right column)\n const questionOptions = useMemo(() => {\n if (!currentQuestion?.options) return [];\n return currentQuestion.options;\n }, [currentQuestion?.options]);\n\n // Build dotsOptions from correctValue of each option (right column values)\n // Shuffle them so they don't appear in the same order as the left column\n const dotsOptions = useMemo(() => {\n const dots = questionOptions\n .map((opt) => ({ label: opt.correctValue || '' }))\n .filter((opt) => opt.label !== '');\n // Use question ID as seed for consistent shuffle\n return shuffleWithSeed(dots, currentQuestion?.id || '');\n }, [questionOptions, currentQuestion?.id]);\n\n // Build options for display (left column with correct answer mapping)\n const options = useMemo(() => {\n return questionOptions.map((opt) => ({\n id: opt.id,\n label: opt.option,\n correctOption: opt.correctValue || '',\n }));\n }, [questionOptions]);\n\n // Parse stored matching answers\n // In result mode, read from matchingAnswers array, fallback to answer JSON\n // In default mode, read from currentAnswer.answer JSON string\n const parsedAnswers: Record<string, string> = useMemo(() => {\n if (\n variant === QuizVariant.RESULT &&\n currentQuestionResult?.matchingAnswers\n ) {\n // Convert matchingAnswers array to Record<optionId, selectedValue>\n const answers: Record<string, string> = {};\n for (const match of currentQuestionResult.matchingAnswers) {\n answers[match.optionId] = match.selectedValue;\n }\n return answers;\n }\n // Fallback to answer JSON string (for older/partially migrated submissions)\n return parseStoredAnswers(\n variant,\n currentQuestionResult?.answer,\n currentAnswer?.answer\n );\n }, [\n variant,\n currentQuestionResult?.matchingAnswers,\n currentQuestionResult?.answer,\n currentAnswer?.answer,\n ]);\n\n const [userAnswers, setUserAnswers] = useState<UserAnswer[]>([]);\n\n // Initialize userAnswers from options and stored answers\n useEffect(() => {\n if (options.length > 0) {\n setUserAnswers(\n options.map((option) => {\n const storedValue = parsedAnswers[option.id] || null;\n return {\n option: option.label,\n dotOption: storedValue,\n correctOption: option.correctOption,\n isCorrect: storedValue\n ? storedValue === option.correctOption\n : null,\n };\n })\n );\n }\n }, [options, parsedAnswers]);\n\n const handleSelectDot = (optionIndex: number, dotValue: string) => {\n const option = options[optionIndex];\n if (!option || !currentQuestion) return;\n\n // Update local state\n setUserAnswers((prev) => {\n const next = [...prev];\n next[optionIndex] = {\n option: option.label,\n dotOption: dotValue,\n correctOption: option.correctOption,\n isCorrect: dotValue ? dotValue === option.correctOption : null,\n };\n return next;\n });\n\n // Get the LATEST answers directly from the store (not from closure)\n const latestStoreAnswer = getUserAnswerByQuestionId(currentQuestion.id);\n let baseAnswers: Record<string, string> = {};\n if (latestStoreAnswer?.answer) {\n try {\n const parsed = JSON.parse(latestStoreAnswer.answer);\n baseAnswers = normalizeAnswerData(parsed);\n } catch {\n // Invalid JSON, start fresh\n }\n }\n\n // Persist to shared store as JSON\n const newAnswers = {\n ...baseAnswers,\n ...parsedAnswers,\n [option.id]: dotValue,\n };\n selectDissertativeAnswer(currentQuestion.id, JSON.stringify(newAnswers));\n };\n\n const getLetterByIndex = (index: number) => String.fromCodePoint(97 + index); // 'a', 'b', 'c'...\n\n const isDefaultVariant = variant === QuizVariant.DEFAULT;\n const assignedDots = new Set(\n userAnswers.map((a) => a.dotOption).filter(Boolean)\n );\n\n if (options.length === 0) {\n return (\n <QuizContainer className={cn('', paddingBottom)}>\n <Text size=\"sm\" className=\"text-text-500 italic\">\n Nenhuma opção de relacionamento disponível\n </Text>\n </QuizContainer>\n );\n }\n\n return (\n <>\n <QuizSubTitle subTitle=\"Alternativas\" />\n\n <QuizContainer className={cn('', paddingBottom)}>\n <div className=\"flex flex-col gap-3.5\">\n {options.map((option, index) => {\n const answer = userAnswers[index] || {\n option: option.label,\n dotOption: null,\n correctOption: option.correctOption,\n isCorrect: null,\n };\n const variantCorrect = getAnswerStatus(answer.isCorrect);\n return (\n <section key={option.id} className=\"flex flex-col gap-2\">\n <div\n className={cn(\n 'grid grid-cols-[1fr_auto] items-center gap-4 p-2 rounded-md',\n !isDefaultVariant && variantCorrect\n ? getStatusStyles(variantCorrect)\n : ''\n )}\n >\n <HtmlMathRenderer\n content={prependLetterToHtml(\n getLetterByIndex(index),\n option.label\n )}\n className=\"text-text-900 text-sm\"\n />\n\n {isDefaultVariant ? (\n <Select\n size=\"medium\"\n value={answer.dotOption || undefined}\n onValueChange={(value) => handleSelectDot(index, value)}\n >\n <SelectTrigger\n className=\"w-[180px] overflow-hidden truncate\"\n title={\n answer.dotOption\n ? stripHtmlTags(answer.dotOption)\n : undefined\n }\n >\n <SelectValue placeholder=\"Selecione opção\" />\n </SelectTrigger>\n\n <SelectContent>\n {dotsOptions\n .filter(\n (dot) =>\n !assignedDots.has(dot.label) ||\n answer.dotOption === dot.label\n )\n .map((dot) => (\n <SelectItem\n key={dot.label}\n value={dot.label}\n title={stripHtmlTags(dot.label)}\n truncate\n >\n {stripHtmlTags(dot.label)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n ) : (\n <div>\n {answer.isCorrect === null\n ? null\n : getStatusBadge(variantCorrect)}\n </div>\n )}\n </div>\n\n {!isDefaultVariant && (\n <span className=\"flex flex-row gap-2 items-center\">\n <Text size=\"2xs\" className=\"text-text-800\">\n Resposta selecionada:{' '}\n {answer.dotOption\n ? stripHtmlTags(answer.dotOption)\n : 'Nenhuma'}\n </Text>\n {answer.isCorrect === false && (\n <Text size=\"2xs\" className=\"text-text-800\">\n Resposta correta: {stripHtmlTags(answer.correctOption)}\n </Text>\n )}\n </span>\n )}\n </section>\n );\n })}\n </div>\n </QuizContainer>\n </>\n );\n};\n\nconst QuizFill = ({ paddingBottom }: QuizVariantInterface) => {\n const {\n getCurrentQuestion,\n getQuestionResultByQuestionId,\n getCurrentAnswer,\n selectDissertativeAnswer,\n variant,\n getUserAnswerByQuestionId,\n } = useQuizStore();\n\n const currentQuestion = getCurrentQuestion();\n const currentQuestionResult = getQuestionResultByQuestionId(\n currentQuestion?.id || ''\n );\n const currentAnswer = getCurrentAnswer();\n\n const baseId = useId();\n\n // Get the additionalContent from the question (contains text with {optionId} placeholders)\n // In result mode, prefer the persisted snapshot to match parsedAnswers\n const additionalContent =\n variant === QuizVariant.RESULT\n ? currentQuestionResult?.additionalContent ||\n currentQuestion?.additionalContent ||\n ''\n : currentQuestion?.additionalContent || '';\n\n // Get the options from the question\n // In result mode, prefer the persisted snapshot to match parsedAnswers\n const questionOptions =\n variant === QuizVariant.RESULT\n ? currentQuestionResult?.options || currentQuestion?.options || []\n : currentQuestion?.options || [];\n\n // Shuffled options for dropdown display (consistent per question)\n const shuffledOptions = useMemo(() => {\n return shuffleWithSeed(questionOptions, currentQuestion?.id || '');\n }, [questionOptions, currentQuestion?.id]);\n\n // Parse current answers\n // In result mode, read from fillAnswers object, fallback to answer JSON\n // In default mode, read from currentAnswer.answer JSON string\n const parsedAnswers: Record<string, string> = useMemo(() => {\n if (variant === QuizVariant.RESULT && currentQuestionResult?.fillAnswers) {\n // fillAnswers is already a Record<placeholderId, selectedOptionId>\n return currentQuestionResult.fillAnswers;\n }\n // Fallback to answer JSON string (for older/partially migrated submissions)\n return parseStoredAnswers(\n variant,\n currentQuestionResult?.answer,\n currentAnswer?.answer\n );\n }, [\n variant,\n currentQuestionResult?.fillAnswers,\n currentQuestionResult?.answer,\n currentAnswer?.answer,\n ]);\n\n const [localAnswers, setLocalAnswers] =\n useState<Record<string, string>>(parsedAnswers);\n\n // Sync local answers when question changes\n useEffect(() => {\n setLocalAnswers(parsedAnswers);\n }, [parsedAnswers, currentQuestion?.id]);\n\n // Handle select change\n const handleSelectChange = (placeholderId: string, optionId: string) => {\n if (!currentQuestion) return;\n\n // Get the LATEST answers directly from the store (not from closure)\n const latestStoreAnswer = getUserAnswerByQuestionId(currentQuestion.id);\n let baseAnswers: Record<string, string> = {};\n if (latestStoreAnswer?.answer) {\n try {\n const parsed = JSON.parse(latestStoreAnswer.answer);\n baseAnswers = normalizeAnswerData(parsed);\n } catch {\n // Invalid JSON, start fresh\n }\n }\n\n const newAnswers = {\n ...baseAnswers,\n ...localAnswers,\n [placeholderId]: optionId,\n };\n setLocalAnswers(newAnswers);\n\n // Save to store as JSON string\n selectDissertativeAnswer(currentQuestion.id, JSON.stringify(newAnswers));\n };\n\n // Get option text by ID\n const getOptionTextById = (optionId: string): string => {\n const option = questionOptions.find((opt) => opt.id === optionId);\n return stripHtmlTags(option?.option || '');\n };\n\n // Check if an answer is correct (for result mode)\n const isAnswerCorrect = (placeholderId: string): boolean => {\n const selectedOptionId = localAnswers[placeholderId];\n // The placeholder ID is the correct option ID\n return selectedOptionId === placeholderId;\n };\n\n // Render the select for default mode\n const renderDefaultSelect = (placeholderId: string) => {\n const selectedOptionId = localAnswers[placeholderId];\n\n return (\n <span\n key={placeholderId}\n className=\"inline-block align-middle mx-1 my-2\"\n style={{ display: 'inline-block', verticalAlign: 'middle' }}\n >\n <Select\n value={selectedOptionId || undefined}\n onValueChange={(value) => handleSelectChange(placeholderId, value)}\n >\n <SelectTrigger\n className=\"w-auto min-w-[120px] max-w-[200px] h-7 px-2 bg-background border-gray-300 overflow-hidden truncate\"\n title={stripHtmlTags(\n shuffledOptions.find((o) => o.id === selectedOptionId)?.option ||\n ''\n )}\n >\n <SelectValue placeholder=\"Selecione opção\" />\n </SelectTrigger>\n <SelectContent>\n {shuffledOptions.map((option) => (\n <SelectItem\n key={option.id}\n value={option.id}\n title={stripHtmlTags(option.option)}\n truncate\n >\n {stripHtmlTags(option.option)}\n </SelectItem>\n ))}\n </SelectContent>\n </Select>\n </span>\n );\n };\n\n // Render the result badge\n const renderResultBadge = (placeholderId: string) => {\n const selectedOptionId = localAnswers[placeholderId];\n const selectedOptionText = getOptionTextById(selectedOptionId);\n const isCorrect = isAnswerCorrect(placeholderId);\n\n if (!selectedOptionId) {\n return (\n <span\n key={placeholderId}\n className=\"inline-block align-middle mx-1 my-2\"\n style={{ display: 'inline-block', verticalAlign: 'middle' }}\n >\n <Badge\n variant=\"solid\"\n action=\"error\"\n iconRight={<XCircleIcon aria-hidden=\"true\" />}\n size=\"large\"\n className=\"py-1 px-2\"\n >\n Não respondido\n </Badge>\n </span>\n );\n }\n\n return (\n <span\n key={placeholderId}\n className=\"inline-block align-middle mx-1 my-2\"\n style={{ display: 'inline-block', verticalAlign: 'middle' }}\n >\n <Badge\n variant=\"solid\"\n action={isCorrect ? 'success' : 'error'}\n iconRight={\n isCorrect ? (\n <CheckCircleIcon aria-hidden=\"true\" />\n ) : (\n <XCircleIcon aria-hidden=\"true\" />\n )\n }\n size=\"large\"\n className=\"py-1 px-2\"\n >\n <span className=\"text-text-900\">{selectedOptionText}</span>\n {/* O certo/errado só aparece pela cor e pelo ícone; o leitor de\n tela precisa do resultado em texto (WCAG 1.4.1). */}\n <Text as=\"span\" className=\"sr-only\">\n {isCorrect ? ' — correta' : ' — incorreta'}\n </Text>\n </Badge>\n </span>\n );\n };\n\n // Render the correct answer for resolution\n const renderResolutionAnswer = (placeholderId: string) => {\n // The placeholderId IS the correct option ID\n const correctOptionText = getOptionTextById(placeholderId);\n\n return (\n <span className=\"inline mx-1 text-success-600 font-semibold border-b-2 border-success-600\">\n {/* O destaque verde não chega ao leitor de tela: o prefixo diz que\n este é o gabarito da lacuna. */}\n <Text as=\"span\" className=\"sr-only\">\n Resposta correta:{' '}\n </Text>\n {correctOptionText}\n </span>\n );\n };\n\n // Parse text and render with selects\n const renderTextWithSelects = (text: string, isResolution?: boolean) => {\n const elements: Array<{ element: string | ReactNode; id: string }> = [];\n let lastIndex = 0;\n const nextId = () => elements.length;\n\n // Match {uuid} placeholders (UUID format or any alphanumeric with hyphens)\n const regex = /\\{([a-zA-Z0-9-]+)\\}/g;\n let match;\n\n while ((match = regex.exec(text)) !== null) {\n const [fullMatch, placeholderId] = match;\n const startIndex = match.index;\n\n // Add text before the placeholder\n if (startIndex > lastIndex) {\n elements.push({\n element: text.slice(lastIndex, startIndex),\n id: `${baseId}-text-${nextId()}`,\n });\n }\n\n // Add the appropriate element based on variant and mode\n if (isResolution) {\n elements.push({\n element: renderResolutionAnswer(placeholderId),\n id: `${baseId}-resolution-${nextId()}`,\n });\n } else if (variant === QuizVariant.DEFAULT) {\n elements.push({\n element: renderDefaultSelect(placeholderId),\n id: `${baseId}-select-${nextId()}`,\n });\n } else {\n elements.push({\n element: renderResultBadge(placeholderId),\n id: `${baseId}-result-${nextId()}`,\n });\n }\n\n lastIndex = match.index + fullMatch.length;\n }\n\n // Add remaining text\n if (lastIndex < text.length) {\n elements.push({\n element: text.slice(lastIndex),\n id: `${baseId}-text-${nextId()}`,\n });\n }\n\n return elements;\n };\n\n // Render HTML content with selects replacing placeholders\n const renderHtmlWithSelects = (\n htmlContent: string,\n isResolution?: boolean\n ) => {\n // For now, we'll strip HTML and render plain text with selects\n // This preserves the placeholder functionality while handling HTML content\n const textContent = stripHtmlTags(htmlContent);\n return renderTextWithSelects(textContent, isResolution);\n };\n\n if (!currentQuestion || !additionalContent) {\n return (\n <>\n <QuizSubTitle subTitle=\"Preenchimento\" />\n <QuizContainer className=\"h-auto pb-0\">\n <div className=\"space-y-6 px-4 h-auto\">\n <Text\n size=\"md\"\n color=\"text-text-600\"\n weight=\"normal\"\n className={cn(paddingBottom)}\n >\n Nenhum conteúdo disponível para esta questão.\n </Text>\n </div>\n </QuizContainer>\n </>\n );\n }\n\n return (\n <>\n <QuizSubTitle subTitle=\"Preencha as lacunas\" />\n\n <QuizContainer\n className={cn('', variant !== QuizVariant.RESULT && paddingBottom)}\n >\n <div className=\"px-4\">\n <Text\n as=\"div\"\n size=\"lg\"\n color=\"text-text-900\"\n weight=\"normal\"\n className=\"leading-[2.5] *:inline\"\n >\n {renderHtmlWithSelects(additionalContent).map((element) => (\n <Fragment key={element.id}>{element.element}</Fragment>\n ))}\n </Text>\n </div>\n </QuizContainer>\n\n {variant === QuizVariant.RESULT && (\n <>\n <QuizSubTitle subTitle=\"Resposta correta\" />\n\n <QuizContainer className={cn('', paddingBottom)}>\n <div className=\"px-4\">\n <Text\n as=\"div\"\n size=\"lg\"\n color=\"text-text-900\"\n weight=\"normal\"\n className=\"leading-[2.5] *:inline\"\n >\n {renderHtmlWithSelects(additionalContent, true).map(\n (element) => (\n <Fragment key={element.id}>{element.element}</Fragment>\n )\n )}\n </Text>\n </div>\n </QuizContainer>\n </>\n )}\n </>\n );\n};\n\nconst QuizImageQuestion = ({ paddingBottom }: QuizVariantInterface) => {\n const {\n variant,\n getCurrentQuestion,\n getCurrentAnswer,\n selectDissertativeAnswer,\n getQuestionResultByQuestionId,\n } = useQuizStore();\n\n const currentQuestion = getCurrentQuestion();\n const currentAnswer = getCurrentAnswer();\n const currentQuestionResult = getQuestionResultByQuestionId(\n currentQuestion?.id || ''\n );\n\n // Get image URL from additionalContent\n const imageUrl = currentQuestion?.additionalContent || '';\n\n // Parse correct coordinates from first option (stored as JSON: {\"x\": number, \"y\": number})\n const correctPositionRelative = useMemo(() => {\n if (!currentQuestion?.options || currentQuestion.options.length === 0) {\n return { x: 0.5, y: 0.5 }; // Default center\n }\n try {\n const coords = JSON.parse(currentQuestion.options[0].option);\n if (typeof coords.x === 'number' && typeof coords.y === 'number') {\n // Coordinates are stored as percentages (0-100), convert to relative (0-1)\n return { x: coords.x / 100, y: coords.y / 100 };\n }\n } catch {\n // Fall back to default\n }\n return { x: 0.5, y: 0.5 };\n }, [currentQuestion?.options]);\n\n // Calculate correctRadiusRelative automatically based on the circle dimensions\n const calculateCorrectRadiusRelative = (): number => {\n // The correct answer circle has width: 15% and height: 30%\n // We'll use the average of these as the radius\n const circleWidthRelative = 0.15; // 15%\n const circleHeightRelative = 0.3; // 30%\n\n // Calculate the average radius (half of the average of width and height)\n const averageRadius = (circleWidthRelative + circleHeightRelative) / 4;\n\n // Add a small tolerance for better user experience\n const tolerance = 0.02; // 2% tolerance\n\n return averageRadius + tolerance;\n };\n\n const correctRadiusRelative = calculateCorrectRadiusRelative();\n\n // Parse user's answer from stored JSON or result\n const storedUserAnswer = useMemo(() => {\n if (variant === QuizVariant.RESULT && currentQuestionResult?.answer) {\n try {\n const coords = JSON.parse(currentQuestionResult.answer);\n if (typeof coords.x === 'number' && typeof coords.y === 'number') {\n return { x: coords.x / 100, y: coords.y / 100 };\n }\n } catch {\n // Fall back to null\n }\n } else if (currentAnswer?.answer) {\n try {\n const coords = JSON.parse(currentAnswer.answer);\n if (typeof coords.x === 'number' && typeof coords.y === 'number') {\n return { x: coords.x / 100, y: coords.y / 100 };\n }\n } catch {\n // Fall back to null\n }\n }\n return null;\n }, [variant, currentQuestionResult?.answer, currentAnswer?.answer]);\n\n const [clickPositionRelative, setClickPositionRelative] = useState<{\n x: number;\n y: number;\n } | null>(storedUserAnswer);\n\n // Sync with stored answer when it changes\n useEffect(() => {\n setClickPositionRelative(storedUserAnswer);\n }, [storedUserAnswer]);\n\n // Helper function to safely convert click coordinates to relative coordinates\n const convertToRelativeCoordinates = (\n x: number,\n y: number,\n rect: DOMRect\n ): { x: number; y: number } => {\n // Guard against division by zero or extremely small dimensions\n const safeWidth = Math.max(rect.width, 0.001);\n const safeHeight = Math.max(rect.height, 0.001);\n\n // Convert to relative coordinates and clamp to [0, 1] range\n const xRelative = Math.max(0, Math.min(1, x / safeWidth));\n const yRelative = Math.max(0, Math.min(1, y / safeHeight));\n\n return { x: xRelative, y: yRelative };\n };\n\n const handleImageClick = (event: MouseEvent<HTMLButtonElement>) => {\n if (variant === QuizVariant.RESULT) return;\n\n const rect = event.currentTarget.getBoundingClientRect();\n const x = event.clientX - rect.left;\n const y = event.clientY - rect.top;\n\n // Use helper function for safe conversion\n const positionRelative = convertToRelativeCoordinates(x, y, rect);\n setClickPositionRelative(positionRelative);\n\n // Save the answer to the store (as percentages 0-100)\n if (currentQuestion) {\n const answerJson = JSON.stringify({\n x: Math.round(positionRelative.x * 100),\n y: Math.round(positionRelative.y * 100),\n });\n selectDissertativeAnswer(currentQuestion.id, answerJson);\n }\n };\n\n const handleKeyboardActivate = () => {\n if (variant === QuizVariant.RESULT) return;\n // Choose a deterministic position for keyboard activation; center is a reasonable default\n const centerPosition = { x: 0.5, y: 0.5 };\n setClickPositionRelative(centerPosition);\n\n // Save the answer to the store (as percentages 0-100)\n if (currentQuestion) {\n const answerJson = JSON.stringify({\n x: Math.round(centerPosition.x * 100),\n y: Math.round(centerPosition.y * 100),\n });\n selectDissertativeAnswer(currentQuestion.id, answerJson);\n }\n };\n\n const isCorrect = () => {\n if (!clickPositionRelative) return false;\n\n const distance = Math.sqrt(\n Math.pow(clickPositionRelative.x - correctPositionRelative.x, 2) +\n Math.pow(clickPositionRelative.y - correctPositionRelative.y, 2)\n );\n\n return distance <= correctRadiusRelative;\n };\n\n const getUserCircleColorClasses = () => {\n if (variant === QuizVariant.DEFAULT) {\n return 'bg-indicator-primary/70 border-[#F8CC2E]';\n }\n\n if (variant === QuizVariant.RESULT) {\n return isCorrect()\n ? 'bg-success-600/70 border-white' // Green for correct answer\n : 'bg-indicator-error/70 border-white'; // Red for incorrect answer\n }\n\n return 'bg-success-600/70 border-white';\n };\n\n // In review the question is read, not answered: the shared view draws the\n // image, the answer area and the student's click with the very tolerance the\n // backend graded with. This screen used to draw its own circle with a radius\n // of its own, so the drawing could disagree with the score.\n if (variant === QuizVariant.RESULT) {\n return (\n <QuizContainer className={cn('', paddingBottom)}>\n <div data-testid=\"quiz-image-container\" className=\"p-3\">\n <ImageAnswerView\n imageUrl={imageUrl}\n correctPoint={parseImageCorrectPoint(currentQuestion?.options)}\n studentPoint={parseImageStudentPoint(currentQuestionResult)}\n toleranceRadius={\n currentQuestionResult?.imageTolerance ?? DEFAULT_IMAGE_TOLERANCE\n }\n />\n </div>\n </QuizContainer>\n );\n }\n\n return (\n <>\n <QuizSubTitle subTitle=\"Clique na área correta\" />\n\n <QuizContainer className={cn('', paddingBottom)}>\n <div\n data-testid=\"quiz-image-container\"\n className=\"space-y-6 p-3 relative inline-block\"\n >\n <button\n data-testid=\"quiz-image-button\"\n type=\"button\"\n className=\"relative cursor-pointer w-full h-full border-0 bg-transparent p-0\"\n onClick={handleImageClick}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n handleKeyboardActivate();\n }\n }}\n aria-label=\"Área da imagem interativa\"\n >\n <img\n data-testid=\"quiz-image\"\n src={imageUrl || ImageQuestion}\n alt=\"Question\"\n className=\"w-full h-auto rounded-md\"\n />\n\n {/* User's answer circle */}\n {clickPositionRelative && (\n <div\n data-testid=\"quiz-user-circle\"\n className={`absolute rounded-full border-4 pointer-events-none ${getUserCircleColorClasses()}`}\n style={{\n minWidth: '30px',\n maxWidth: '52px',\n width: '5%',\n aspectRatio: '1 / 1',\n left: `calc(${clickPositionRelative.x * 100}% - 2.5%)`,\n top: `calc(${clickPositionRelative.y * 100}% - 2.5%)`,\n }}\n />\n )}\n </button>\n </div>\n </QuizContainer>\n </>\n );\n};\n\nexport {\n QuizSubTitle,\n QuizContainer,\n QuizMultipleChoice,\n QuizDissertative,\n QuizTrueOrFalse,\n QuizConnectDots,\n QuizFill,\n QuizImageQuestion,\n QuizAlternative,\n};\n","import type {\n Question,\n QuestionResult,\n} from '../../components/Quiz/useQuizStore';\nimport type { ImagePoint } from '../../components/shared/ImageAnswerView';\n\n/**\n * Fallback radius, in image percentage points, used when the payload does not\n * carry the backend's tolerance.\n *\n * The backend is the authority — it grades with its own value and now sends it\n * as `imageTolerance`. This only covers older payloads, and matches the value\n * the backend has always used.\n */\nexport const DEFAULT_IMAGE_TOLERANCE = 10;\n\n/**\n * Read the answer key point of an IMAGEM question.\n *\n * The key lives in the question's single option, whose text is the JSON\n * `{\"x\": number, \"y\": number}` in percentage points of the image.\n *\n * @param options - The question's options\n * @returns The correct point, or null when there is no usable key\n *\n * @example\n * ```typescript\n * parseImageCorrectPoint([{ id: 'o1', option: '{\"x\":50,\"y\":30}' }]); // { x: 50, y: 30 }\n * ```\n */\nexport const parseImageCorrectPoint = (\n options: Question['options'] | undefined\n): ImagePoint | null => {\n const first = options?.[0];\n if (!first?.option) {\n return null;\n }\n\n try {\n const parsed = JSON.parse(first.option);\n if (typeof parsed?.x === 'number' && typeof parsed?.y === 'number') {\n return { x: parsed.x, y: parsed.y };\n }\n } catch {\n return null;\n }\n\n return null;\n};\n\n/**\n * Read where the student clicked on an IMAGEM question.\n *\n * Prefers the structured `imageAnswer` the backend sends. Falls back to parsing\n * `answer`, which older payloads and the draft flow still use, in either the\n * `{x, y}` or `{coordinateX, coordinateY}` shape.\n *\n * @param result - The student's answer for the question\n * @returns The clicked point in percentage points (0-100), or null\n *\n * @example\n * ```typescript\n * parseImageStudentPoint({ imageAnswer: { coordinateX: 52, coordinateY: 28 } }); // { x: 52, y: 28 }\n * ```\n */\nexport const parseImageStudentPoint = (\n result: QuestionResult['answers'][number] | null | undefined\n): ImagePoint | null => {\n if (!result) {\n return null;\n }\n\n if (result.imageAnswer) {\n const { coordinateX, coordinateY } = result.imageAnswer;\n if (typeof coordinateX === 'number' && typeof coordinateY === 'number') {\n return { x: coordinateX, y: coordinateY };\n }\n }\n\n if (!result.answer) {\n return null;\n }\n\n try {\n const parsed = JSON.parse(result.answer);\n const x =\n typeof parsed?.coordinateX === 'number' ? parsed.coordinateX : parsed?.x;\n const y =\n typeof parsed?.coordinateY === 'number' ? parsed.coordinateY : parsed?.y;\n if (typeof x === 'number' && typeof y === 'number') {\n return { x, y };\n }\n } catch {\n return null;\n }\n\n return null;\n};\n"]}
@@ -14,7 +14,7 @@ import {
14
14
  } from "./chunk-VN4ORBHU.mjs";
15
15
  import {
16
16
  Modal_default
17
- } from "./chunk-XZUMD3CB.mjs";
17
+ } from "./chunk-LODDYDTP.mjs";
18
18
  import {
19
19
  Button_default
20
20
  } from "./chunk-ZQZBOTZJ.mjs";
@@ -160,4 +160,4 @@ var AlertsManagerView = ({
160
160
  export {
161
161
  AlertsManagerView
162
162
  };
163
- //# sourceMappingURL=chunk-C65DG2RI.mjs.map
163
+ //# sourceMappingURL=chunk-7FSBWUOM.mjs.map