@hed-hog/lms 0.0.361 → 0.0.364
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bitcode-wallet/bitcode-wallet.service.d.ts +65 -0
- package/dist/bitcode-wallet/bitcode-wallet.service.d.ts.map +1 -1
- package/dist/bitcode-wallet/bitcode-wallet.service.js +72 -0
- package/dist/bitcode-wallet/bitcode-wallet.service.js.map +1 -1
- package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.d.ts +8 -0
- package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.d.ts.map +1 -0
- package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.js +40 -0
- package/dist/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.js.map +1 -0
- package/dist/class-group/class-group.controller.d.ts +16 -16
- package/dist/class-group/class-group.service.d.ts +12 -12
- package/dist/course/course-audio-transcription.service.d.ts +3 -2
- package/dist/course/course-audio-transcription.service.d.ts.map +1 -1
- package/dist/course/course-audio-transcription.service.js +49 -8
- package/dist/course/course-audio-transcription.service.js.map +1 -1
- package/dist/course/course-lesson.controller.d.ts +4 -0
- package/dist/course/course-lesson.controller.d.ts.map +1 -1
- package/dist/course/course-lesson.controller.js +10 -0
- package/dist/course/course-lesson.controller.js.map +1 -1
- package/dist/course/course-structure.controller.d.ts +11 -4
- package/dist/course/course-structure.controller.d.ts.map +1 -1
- package/dist/course/course-structure.controller.js +14 -0
- package/dist/course/course-structure.controller.js.map +1 -1
- package/dist/course/course-structure.service.d.ts +15 -1
- package/dist/course/course-structure.service.d.ts.map +1 -1
- package/dist/course/course-structure.service.js +139 -4
- package/dist/course/course-structure.service.js.map +1 -1
- package/dist/course/course-video-conversion.service.d.ts +8 -0
- package/dist/course/course-video-conversion.service.d.ts.map +1 -1
- package/dist/course/course-video-conversion.service.js +87 -51
- package/dist/course/course-video-conversion.service.js.map +1 -1
- package/dist/course/course.controller.d.ts +73 -1
- package/dist/course/course.controller.d.ts.map +1 -1
- package/dist/course/course.controller.js +27 -3
- package/dist/course/course.controller.js.map +1 -1
- package/dist/course/course.module.d.ts.map +1 -1
- package/dist/course/course.module.js +2 -0
- package/dist/course/course.module.js.map +1 -1
- package/dist/course/course.service.d.ts +108 -4
- package/dist/course/course.service.d.ts.map +1 -1
- package/dist/course/course.service.js +631 -30
- package/dist/course/course.service.js.map +1 -1
- package/dist/course/dto/cleanup-course-storage.dto.d.ts +6 -0
- package/dist/course/dto/cleanup-course-storage.dto.d.ts.map +1 -0
- package/dist/course/dto/cleanup-course-storage.dto.js +33 -0
- package/dist/course/dto/cleanup-course-storage.dto.js.map +1 -0
- package/dist/course/dto/cleanup-upload-history.dto.d.ts +9 -0
- package/dist/course/dto/cleanup-upload-history.dto.d.ts.map +1 -0
- package/dist/course/dto/cleanup-upload-history.dto.js +36 -0
- package/dist/course/dto/cleanup-upload-history.dto.js.map +1 -0
- package/dist/course/dto/create-course-bulk-job.dto.d.ts +4 -0
- package/dist/course/dto/create-course-bulk-job.dto.d.ts.map +1 -0
- package/dist/course/dto/create-course-bulk-job.dto.js +21 -0
- package/dist/course/dto/create-course-bulk-job.dto.js.map +1 -0
- package/dist/course/lms-bulk-upload-automation.service.d.ts +39 -0
- package/dist/course/lms-bulk-upload-automation.service.d.ts.map +1 -0
- package/dist/course/lms-bulk-upload-automation.service.js +443 -0
- package/dist/course/lms-bulk-upload-automation.service.js.map +1 -0
- package/dist/course/lms-bulk-upload-infra.service.d.ts +31 -0
- package/dist/course/lms-bulk-upload-infra.service.d.ts.map +1 -0
- package/dist/course/lms-bulk-upload-infra.service.js +277 -0
- package/dist/course/lms-bulk-upload-infra.service.js.map +1 -0
- package/dist/course/lms-bulk-upload.constants.d.ts +4 -0
- package/dist/course/lms-bulk-upload.constants.d.ts.map +1 -0
- package/dist/course/lms-bulk-upload.constants.js +7 -0
- package/dist/course/lms-bulk-upload.constants.js.map +1 -0
- package/dist/course/lms-bulk-upload.controller.d.ts +116 -0
- package/dist/course/lms-bulk-upload.controller.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload.controller.js +71 -2
- package/dist/course/lms-bulk-upload.controller.js.map +1 -1
- package/dist/course/lms-bulk-upload.service.d.ts +142 -3
- package/dist/course/lms-bulk-upload.service.d.ts.map +1 -1
- package/dist/course/lms-bulk-upload.service.js +606 -21
- package/dist/course/lms-bulk-upload.service.js.map +1 -1
- package/dist/course/lms-setting.controller.d.ts +4 -1
- package/dist/course/lms-setting.controller.d.ts.map +1 -1
- package/dist/course/lms-setting.controller.js +21 -6
- package/dist/course/lms-setting.controller.js.map +1 -1
- package/dist/enterprise/enterprise.controller.d.ts +20 -20
- package/dist/enterprise/enterprise.service.d.ts +20 -20
- package/dist/enterprise/training/training-admin.controller.d.ts +11 -11
- package/dist/enterprise/training/training-admin.service.d.ts +11 -11
- package/dist/enterprise/training/training-instructor.controller.d.ts +2 -2
- package/dist/enterprise/training/training-instructor.service.d.ts +2 -2
- package/dist/enterprise/training/training-student.controller.d.ts +1 -1
- package/dist/enterprise/training/training-student.service.d.ts +1 -1
- package/dist/enterprise/training/training-viewer.controller.d.ts +2 -2
- package/dist/evaluation/evaluation.controller.d.ts +8 -8
- package/dist/evaluation/evaluation.service.d.ts +8 -8
- package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.d.ts +6 -0
- package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.d.ts.map +1 -0
- package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.js +34 -0
- package/dist/lesson-xp-map/dto/create-lesson-xp-map.dto.js.map +1 -0
- package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.d.ts +28 -0
- package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.d.ts.map +1 -0
- package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.js +123 -0
- package/dist/lesson-xp-map/dto/create-lesson-xp-segment.dto.js.map +1 -0
- package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.d.ts +4 -0
- package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.d.ts.map +1 -0
- package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.js +22 -0
- package/dist/lesson-xp-map/dto/review-lesson-xp-map.dto.js.map +1 -0
- package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.d.ts +10 -0
- package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.d.ts.map +1 -0
- package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.js +52 -0
- package/dist/lesson-xp-map/dto/update-lesson-xp-map.dto.js.map +1 -0
- package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.d.ts +15 -0
- package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.d.ts.map +1 -0
- package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.js +86 -0
- package/dist/lesson-xp-map/dto/update-lesson-xp-segment.dto.js.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts +26 -0
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.d.ts.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js +304 -0
- package/dist/lesson-xp-map/lesson-xp-ai-calculation.service.js.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-map.controller.d.ts +87 -0
- package/dist/lesson-xp-map/lesson-xp-map.controller.d.ts.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-map.controller.js +185 -0
- package/dist/lesson-xp-map/lesson-xp-map.controller.js.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-map.module.d.ts +3 -0
- package/dist/lesson-xp-map/lesson-xp-map.module.d.ts.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-map.module.js +34 -0
- package/dist/lesson-xp-map/lesson-xp-map.module.js.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-map.service.d.ts +84 -0
- package/dist/lesson-xp-map/lesson-xp-map.service.d.ts.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-map.service.js +353 -0
- package/dist/lesson-xp-map/lesson-xp-map.service.js.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-segment.controller.d.ts +10 -0
- package/dist/lesson-xp-map/lesson-xp-segment.controller.d.ts.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-segment.controller.js +63 -0
- package/dist/lesson-xp-map/lesson-xp-segment.controller.js.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-segment.service.d.ts +27 -0
- package/dist/lesson-xp-map/lesson-xp-segment.service.d.ts.map +1 -0
- package/dist/lesson-xp-map/lesson-xp-segment.service.js +194 -0
- package/dist/lesson-xp-map/lesson-xp-segment.service.js.map +1 -0
- package/dist/libraries/lms/tsconfig.tsbuildinfo +1 -0
- package/dist/lms.module.d.ts.map +1 -1
- package/dist/lms.module.js +17 -2
- package/dist/lms.module.js.map +1 -1
- package/dist/platforma/dto/update-profile.dto.d.ts +17 -0
- package/dist/platforma/dto/update-profile.dto.d.ts.map +1 -0
- package/dist/platforma/dto/update-profile.dto.js +87 -0
- package/dist/platforma/dto/update-profile.dto.js.map +1 -0
- package/dist/platforma/platforma.controller.d.ts +88 -1
- package/dist/platforma/platforma.controller.d.ts.map +1 -1
- package/dist/platforma/platforma.controller.js +85 -2
- package/dist/platforma/platforma.controller.js.map +1 -1
- package/dist/platforma/platforma.service.d.ts +27 -0
- package/dist/platforma/platforma.service.d.ts.map +1 -0
- package/dist/platforma/platforma.service.js +274 -0
- package/dist/platforma/platforma.service.js.map +1 -0
- package/dist/student-xp/student-xp.controller.d.ts +41 -0
- package/dist/student-xp/student-xp.controller.d.ts.map +1 -0
- package/dist/student-xp/student-xp.controller.js +114 -0
- package/dist/student-xp/student-xp.controller.js.map +1 -0
- package/dist/student-xp/student-xp.module.d.ts +3 -0
- package/dist/student-xp/student-xp.module.d.ts.map +1 -0
- package/dist/student-xp/student-xp.module.js +25 -0
- package/dist/student-xp/student-xp.module.js.map +1 -0
- package/dist/student-xp/student-xp.service.d.ts +65 -0
- package/dist/student-xp/student-xp.service.d.ts.map +1 -0
- package/dist/student-xp/student-xp.service.js +197 -0
- package/dist/student-xp/student-xp.service.js.map +1 -0
- package/dist/xp-catalog/dto/create-xp-area.dto.d.ts +12 -0
- package/dist/xp-catalog/dto/create-xp-area.dto.d.ts.map +1 -0
- package/dist/xp-catalog/dto/create-xp-area.dto.js +63 -0
- package/dist/xp-catalog/dto/create-xp-area.dto.js.map +1 -0
- package/dist/xp-catalog/dto/create-xp-learning-type.dto.d.ts +11 -0
- package/dist/xp-catalog/dto/create-xp-learning-type.dto.d.ts.map +1 -0
- package/dist/xp-catalog/dto/create-xp-learning-type.dto.js +57 -0
- package/dist/xp-catalog/dto/create-xp-learning-type.dto.js.map +1 -0
- package/dist/xp-catalog/dto/create-xp-skill.dto.d.ts +11 -0
- package/dist/xp-catalog/dto/create-xp-skill.dto.d.ts.map +1 -0
- package/dist/xp-catalog/dto/create-xp-skill.dto.js +57 -0
- package/dist/xp-catalog/dto/create-xp-skill.dto.js.map +1 -0
- package/dist/xp-catalog/dto/update-xp-area.dto.d.ts +12 -0
- package/dist/xp-catalog/dto/update-xp-area.dto.d.ts.map +1 -0
- package/dist/xp-catalog/dto/update-xp-area.dto.js +66 -0
- package/dist/xp-catalog/dto/update-xp-area.dto.js.map +1 -0
- package/dist/xp-catalog/dto/update-xp-learning-type.dto.d.ts +11 -0
- package/dist/xp-catalog/dto/update-xp-learning-type.dto.d.ts.map +1 -0
- package/dist/xp-catalog/dto/update-xp-learning-type.dto.js +60 -0
- package/dist/xp-catalog/dto/update-xp-learning-type.dto.js.map +1 -0
- package/dist/xp-catalog/dto/update-xp-skill.dto.d.ts +11 -0
- package/dist/xp-catalog/dto/update-xp-skill.dto.d.ts.map +1 -0
- package/dist/xp-catalog/dto/update-xp-skill.dto.js +60 -0
- package/dist/xp-catalog/dto/update-xp-skill.dto.js.map +1 -0
- package/dist/xp-catalog/xp-area.controller.d.ts +25 -0
- package/dist/xp-catalog/xp-area.controller.d.ts.map +1 -0
- package/dist/xp-catalog/xp-area.controller.js +105 -0
- package/dist/xp-catalog/xp-area.controller.js.map +1 -0
- package/dist/xp-catalog/xp-area.service.d.ts +35 -0
- package/dist/xp-catalog/xp-area.service.d.ts.map +1 -0
- package/dist/xp-catalog/xp-area.service.js +168 -0
- package/dist/xp-catalog/xp-area.service.js.map +1 -0
- package/dist/xp-catalog/xp-catalog.module.d.ts +3 -0
- package/dist/xp-catalog/xp-catalog.module.d.ts.map +1 -0
- package/dist/xp-catalog/xp-catalog.module.js +29 -0
- package/dist/xp-catalog/xp-catalog.module.js.map +1 -0
- package/dist/xp-catalog/xp-learning-type.controller.d.ts +20 -0
- package/dist/xp-catalog/xp-learning-type.controller.d.ts.map +1 -0
- package/dist/xp-catalog/xp-learning-type.controller.js +96 -0
- package/dist/xp-catalog/xp-learning-type.controller.js.map +1 -0
- package/dist/xp-catalog/xp-learning-type.service.d.ts +30 -0
- package/dist/xp-catalog/xp-learning-type.service.d.ts.map +1 -0
- package/dist/xp-catalog/xp-learning-type.service.js +146 -0
- package/dist/xp-catalog/xp-learning-type.service.js.map +1 -0
- package/dist/xp-catalog/xp-skill.controller.d.ts +26 -0
- package/dist/xp-catalog/xp-skill.controller.d.ts.map +1 -0
- package/dist/xp-catalog/xp-skill.controller.js +113 -0
- package/dist/xp-catalog/xp-skill.controller.js.map +1 -0
- package/dist/xp-catalog/xp-skill.service.d.ts +37 -0
- package/dist/xp-catalog/xp-skill.service.d.ts.map +1 -0
- package/dist/xp-catalog/xp-skill.service.js +174 -0
- package/dist/xp-catalog/xp-skill.service.js.map +1 -0
- package/hedhog/data/menu.yaml +101 -0
- package/hedhog/data/route.yaml +512 -0
- package/hedhog/data/setting_group.yaml +1 -1
- package/hedhog/data/xp_area.yaml +164 -0
- package/hedhog/data/xp_learning_type.yaml +131 -0
- package/hedhog/data/xp_skill.yaml +1834 -0
- package/hedhog/frontend/app/achievements/page.tsx.ejs +108 -118
- package/hedhog/frontend/app/bitcodes/page.tsx.ejs +22 -34
- package/hedhog/frontend/app/bulk-upload-sessions/page.tsx.ejs +1453 -0
- package/hedhog/frontend/app/certificates/models/page.tsx.ejs +21 -45
- package/hedhog/frontend/app/classes/[id]/page.tsx.ejs +40 -74
- package/hedhog/frontend/app/classes/page.tsx.ejs +56 -85
- package/hedhog/frontend/app/courses/[id]/_components/CourseDangerZoneCard.tsx.ejs +3 -2
- package/hedhog/frontend/app/courses/[id]/_components/CourseMediaCard.tsx.ejs +48 -5
- package/hedhog/frontend/app/courses/[id]/page.tsx.ejs +4 -4
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-operations-tab.tsx.ejs +19 -2
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-overview-tab.tsx.ejs +1170 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +16 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +2 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-xp-overview-tab.tsx.ejs +623 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson-xp-tab.tsx.ejs +1458 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +55 -2
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +442 -104
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +296 -49
- package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +3 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +1 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-display-settings-popover.tsx.ejs +101 -85
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +21 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +3 -0
- package/hedhog/frontend/app/courses/[id]/structure/_components/use-tree-display-settings.ts.ejs +7 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/xp-premium-pills.tsx.ejs +44 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-content-overview.ts.ejs +54 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-structure-mutations.ts.ejs +52 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-course-xp-overview.ts.ejs +76 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-lesson-xp-map.ts.ejs +128 -0
- package/hedhog/frontend/app/courses/[id]/structure/_data/use-transcription-segments.ts.ejs +30 -0
- package/hedhog/frontend/app/courses/[id]/structure/_utils/xp-color-config.ts.ejs +115 -0
- package/hedhog/frontend/app/courses/_components/CourseDeleteDialog.tsx.ejs +223 -0
- package/hedhog/frontend/app/courses/_components/CourseRowActions.tsx.ejs +89 -0
- package/hedhog/frontend/app/courses/page.tsx.ejs +400 -230
- package/hedhog/frontend/app/enterprise/page.tsx.ejs +39 -63
- package/hedhog/frontend/app/exams/[id]/questions/page.tsx.ejs +53 -77
- package/hedhog/frontend/app/exams/page.tsx.ejs +54 -90
- package/hedhog/frontend/app/instructor-skills/page.tsx.ejs +23 -36
- package/hedhog/frontend/app/instructors/page.tsx.ejs +72 -81
- package/hedhog/frontend/app/paths/page.tsx.ejs +40 -68
- package/hedhog/frontend/app/training/page.tsx.ejs +40 -68
- package/hedhog/frontend/app/xp/areas/page.tsx.ejs +782 -0
- package/hedhog/frontend/app/xp/learning-types/page.tsx.ejs +690 -0
- package/hedhog/frontend/app/xp/skills/page.tsx.ejs +811 -0
- package/hedhog/frontend/messages/en.json +386 -3
- package/hedhog/frontend/messages/pt.json +386 -3
- package/hedhog/table/lesson_xp_map.yaml +50 -0
- package/hedhog/table/lesson_xp_segment.yaml +40 -0
- package/hedhog/table/lesson_xp_segment_area.yaml +24 -0
- package/hedhog/table/lesson_xp_segment_learning_type.yaml +24 -0
- package/hedhog/table/lesson_xp_segment_skill.yaml +24 -0
- package/hedhog/table/lms_bulk_upload_item.yaml +44 -0
- package/hedhog/table/lms_bulk_upload_session.yaml +42 -0
- package/hedhog/table/student_area_xp.yaml +30 -0
- package/hedhog/table/student_learning_type_xp.yaml +30 -0
- package/hedhog/table/student_skill_xp.yaml +30 -0
- package/hedhog/table/student_xp_event.yaml +34 -0
- package/hedhog/table/xp_area.yaml +39 -0
- package/hedhog/table/xp_learning_type.yaml +34 -0
- package/hedhog/table/xp_skill.yaml +39 -0
- package/package.json +8 -7
- package/src/bitcode-wallet/bitcode-wallet.service.ts +113 -0
- package/src/bitcode-wallet/dto/create-current-bitcode-wallet-transaction.dto.ts +32 -0
- package/src/course/course-audio-transcription.service.ts +58 -21
- package/src/course/course-lesson.controller.ts +6 -1
- package/src/course/course-structure.controller.ts +10 -0
- package/src/course/course-structure.service.ts +174 -1
- package/src/course/course-video-conversion.service.ts +113 -75
- package/src/course/course.controller.ts +22 -3
- package/src/course/course.module.ts +2 -0
- package/src/course/course.service.ts +847 -30
- package/src/course/dto/cleanup-course-storage.dto.ts +22 -0
- package/src/course/dto/cleanup-upload-history.dto.ts +26 -0
- package/src/course/dto/create-course-bulk-job.dto.ts +6 -0
- package/src/course/lms-bulk-upload-automation.service.ts +560 -0
- package/src/course/lms-bulk-upload-infra.service.ts +327 -0
- package/src/course/lms-bulk-upload.constants.ts +5 -0
- package/src/course/lms-bulk-upload.controller.ts +79 -3
- package/src/course/lms-bulk-upload.service.ts +1029 -204
- package/src/course/lms-setting.controller.ts +22 -6
- package/src/lesson-xp-map/dto/create-lesson-xp-map.dto.ts +17 -0
- package/src/lesson-xp-map/dto/create-lesson-xp-segment.dto.ts +102 -0
- package/src/lesson-xp-map/dto/review-lesson-xp-map.dto.ts +7 -0
- package/src/lesson-xp-map/dto/update-lesson-xp-map.dto.ts +36 -0
- package/src/lesson-xp-map/dto/update-lesson-xp-segment.dto.ts +78 -0
- package/src/lesson-xp-map/lesson-xp-ai-calculation.service.ts +396 -0
- package/src/lesson-xp-map/lesson-xp-map.controller.ts +116 -0
- package/src/lesson-xp-map/lesson-xp-map.module.ts +21 -0
- package/src/lesson-xp-map/lesson-xp-map.service.ts +442 -0
- package/src/lesson-xp-map/lesson-xp-segment.controller.ts +36 -0
- package/src/lesson-xp-map/lesson-xp-segment.service.ts +229 -0
- package/src/lms.module.ts +17 -2
- package/src/platforma/dto/update-profile.dto.ts +59 -0
- package/src/platforma/platforma.controller.ts +57 -2
- package/src/platforma/platforma.service.ts +268 -0
- package/src/student-xp/student-xp.controller.ts +76 -0
- package/src/student-xp/student-xp.module.ts +12 -0
- package/src/student-xp/student-xp.service.ts +236 -0
- package/src/xp-catalog/dto/create-xp-area.dto.ts +40 -0
- package/src/xp-catalog/dto/create-xp-learning-type.dto.ts +35 -0
- package/src/xp-catalog/dto/create-xp-skill.dto.ts +35 -0
- package/src/xp-catalog/dto/update-xp-area.dto.ts +43 -0
- package/src/xp-catalog/dto/update-xp-learning-type.dto.ts +38 -0
- package/src/xp-catalog/dto/update-xp-skill.dto.ts +38 -0
- package/src/xp-catalog/xp-area.controller.ts +64 -0
- package/src/xp-catalog/xp-area.service.ts +196 -0
- package/src/xp-catalog/xp-catalog.module.ts +16 -0
- package/src/xp-catalog/xp-learning-type.controller.ts +59 -0
- package/src/xp-catalog/xp-learning-type.service.ts +170 -0
- package/src/xp-catalog/xp-skill.controller.ts +71 -0
- package/src/xp-catalog/xp-skill.service.ts +205 -0
|
@@ -0,0 +1,1458 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Badge } from '@/components/ui/badge';
|
|
4
|
+
import { Button } from '@/components/ui/button';
|
|
5
|
+
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
|
6
|
+
import { Skeleton } from '@/components/ui/skeleton';
|
|
7
|
+
import {
|
|
8
|
+
Tooltip,
|
|
9
|
+
TooltipContent,
|
|
10
|
+
TooltipProvider,
|
|
11
|
+
TooltipTrigger,
|
|
12
|
+
} from '@/components/ui/tooltip';
|
|
13
|
+
import { cn } from '@/lib/utils';
|
|
14
|
+
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
15
|
+
import {
|
|
16
|
+
AlertCircle,
|
|
17
|
+
BookOpen,
|
|
18
|
+
Brain,
|
|
19
|
+
Clock3,
|
|
20
|
+
Cpu,
|
|
21
|
+
Loader2,
|
|
22
|
+
RefreshCw,
|
|
23
|
+
Zap,
|
|
24
|
+
} from 'lucide-react';
|
|
25
|
+
import { useMemo, useState, type CSSProperties, type ReactNode } from 'react';
|
|
26
|
+
import {
|
|
27
|
+
Cell,
|
|
28
|
+
Pie,
|
|
29
|
+
PieChart,
|
|
30
|
+
ResponsiveContainer,
|
|
31
|
+
Tooltip as RechartsTooltip,
|
|
32
|
+
} from 'recharts';
|
|
33
|
+
|
|
34
|
+
import {
|
|
35
|
+
useLessonXpMapQuery,
|
|
36
|
+
useRecalculateLessonXpMutation,
|
|
37
|
+
type LessonXpMapStatus,
|
|
38
|
+
type LessonXpSegment,
|
|
39
|
+
type LessonXpSegmentLearningType,
|
|
40
|
+
} from '../_data/use-lesson-xp-map';
|
|
41
|
+
import {
|
|
42
|
+
resolveXpAreaColor,
|
|
43
|
+
resolveXpLearningTypeColor,
|
|
44
|
+
resolveXpSkillColor,
|
|
45
|
+
} from '../_utils/xp-color-config';
|
|
46
|
+
|
|
47
|
+
interface LessonXpTabProps {
|
|
48
|
+
lessonId: string;
|
|
49
|
+
activeTab: string;
|
|
50
|
+
hasTranscription: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
type XpAreaOption = {
|
|
54
|
+
id: number;
|
|
55
|
+
name: string;
|
|
56
|
+
color: string | null;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
type XpSkillOption = {
|
|
60
|
+
id: number;
|
|
61
|
+
name: string;
|
|
62
|
+
xpAreaId: number | null;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
type PaginatedResult<T> = {
|
|
66
|
+
data: T[];
|
|
67
|
+
total: number;
|
|
68
|
+
page: number;
|
|
69
|
+
pageSize: number;
|
|
70
|
+
lastPage?: number;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
type DistributionItem = {
|
|
74
|
+
id: number;
|
|
75
|
+
label: string;
|
|
76
|
+
xp: number;
|
|
77
|
+
percent: number;
|
|
78
|
+
color: string;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
type SegmentView = {
|
|
82
|
+
id: number;
|
|
83
|
+
label: string;
|
|
84
|
+
durationSeconds: number;
|
|
85
|
+
startSeconds: number;
|
|
86
|
+
endSeconds: number;
|
|
87
|
+
xpValue: number;
|
|
88
|
+
difficulty: LessonXpSegment['difficulty'];
|
|
89
|
+
shouldGrantXp: boolean;
|
|
90
|
+
aiSummary: string | null;
|
|
91
|
+
aiConfidence: number | null;
|
|
92
|
+
areaLabel: string;
|
|
93
|
+
areaColor: string;
|
|
94
|
+
skillLabel: string;
|
|
95
|
+
skillColor: string;
|
|
96
|
+
learningTypeLabel: string;
|
|
97
|
+
learningTypeColor: string;
|
|
98
|
+
areaBreakdown: Array<{
|
|
99
|
+
key: number;
|
|
100
|
+
label: string;
|
|
101
|
+
percent: number;
|
|
102
|
+
color: string;
|
|
103
|
+
}>;
|
|
104
|
+
skillBreakdown: Array<{
|
|
105
|
+
key: number;
|
|
106
|
+
label: string;
|
|
107
|
+
percent: number;
|
|
108
|
+
color: string;
|
|
109
|
+
}>;
|
|
110
|
+
learningTypeBreakdown: Array<{
|
|
111
|
+
key: number;
|
|
112
|
+
label: string;
|
|
113
|
+
percent: number;
|
|
114
|
+
color: string;
|
|
115
|
+
}>;
|
|
116
|
+
areas: LessonXpSegment['areas'];
|
|
117
|
+
skills: LessonXpSegment['skills'];
|
|
118
|
+
learningTypes: LessonXpSegment['learningTypes'];
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const chartTooltipStyle = {
|
|
122
|
+
backgroundColor: '#ffffff',
|
|
123
|
+
border: '1px solid hsl(var(--border))',
|
|
124
|
+
borderRadius: '10px',
|
|
125
|
+
fontSize: '12px',
|
|
126
|
+
boxShadow: '0 18px 36px -20px rgba(15,23,42,0.42)',
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const chartTooltipWrapperStyle = {
|
|
130
|
+
zIndex: 30,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
export function LessonXpTab({
|
|
134
|
+
lessonId,
|
|
135
|
+
activeTab,
|
|
136
|
+
hasTranscription,
|
|
137
|
+
}: LessonXpTabProps) {
|
|
138
|
+
const { currentLocaleCode, request } = useApp();
|
|
139
|
+
const { data: xpMap, isLoading } = useLessonXpMapQuery(lessonId, activeTab);
|
|
140
|
+
const {
|
|
141
|
+
mutate: recalculate,
|
|
142
|
+
isPending: isRecalculating,
|
|
143
|
+
error: recalcError,
|
|
144
|
+
} = useRecalculateLessonXpMutation(lessonId);
|
|
145
|
+
const [localError, setLocalError] = useState<string | null>(null);
|
|
146
|
+
|
|
147
|
+
const { data: areaOptions = [] } = useQuery<XpAreaOption[]>({
|
|
148
|
+
queryKey: ['lms-xp-areas-all'],
|
|
149
|
+
enabled: activeTab === 'xp',
|
|
150
|
+
queryFn: async () => {
|
|
151
|
+
const response = await request<XpAreaOption[]>({
|
|
152
|
+
url: '/lms/xp/areas/all',
|
|
153
|
+
method: 'GET',
|
|
154
|
+
});
|
|
155
|
+
return response.data;
|
|
156
|
+
},
|
|
157
|
+
placeholderData: [],
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const {
|
|
161
|
+
data: skillPage = {
|
|
162
|
+
data: [],
|
|
163
|
+
total: 0,
|
|
164
|
+
page: 1,
|
|
165
|
+
pageSize: 1000,
|
|
166
|
+
lastPage: 1,
|
|
167
|
+
},
|
|
168
|
+
} = useQuery<PaginatedResult<XpSkillOption>>({
|
|
169
|
+
queryKey: ['lms-xp-skills-all'],
|
|
170
|
+
enabled: activeTab === 'xp',
|
|
171
|
+
queryFn: async () => {
|
|
172
|
+
const response = await request<PaginatedResult<XpSkillOption>>({
|
|
173
|
+
url: '/lms/xp/skills?page=1&pageSize=1000',
|
|
174
|
+
method: 'GET',
|
|
175
|
+
});
|
|
176
|
+
return response.data;
|
|
177
|
+
},
|
|
178
|
+
placeholderData: {
|
|
179
|
+
data: [],
|
|
180
|
+
total: 0,
|
|
181
|
+
page: 1,
|
|
182
|
+
pageSize: 1000,
|
|
183
|
+
lastPage: 1,
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const handleRecalculate = () => {
|
|
188
|
+
setLocalError(null);
|
|
189
|
+
recalculate({
|
|
190
|
+
onError: (msg) => setLocalError(msg),
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const displayError = localError ?? recalcError;
|
|
195
|
+
const isProcessing = xpMap?.status === 'processing';
|
|
196
|
+
|
|
197
|
+
const areaLookup = useMemo(
|
|
198
|
+
() => new Map(areaOptions.map((area) => [area.id, area])),
|
|
199
|
+
[areaOptions]
|
|
200
|
+
);
|
|
201
|
+
const skillLookup = useMemo(
|
|
202
|
+
() => new Map(skillPage.data.map((skill) => [skill.id, skill])),
|
|
203
|
+
[skillPage.data]
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
const areaDistribution = useMemo(() => {
|
|
207
|
+
if (!xpMap) return [];
|
|
208
|
+
return buildDistributionItems(
|
|
209
|
+
aggregateAreas(xpMap.segments),
|
|
210
|
+
xpMap.totalXp,
|
|
211
|
+
(id) => areaLookup.get(id)?.name ?? `Área ${id}`,
|
|
212
|
+
(id, index) =>
|
|
213
|
+
resolveXpAreaColor({
|
|
214
|
+
areaId: id,
|
|
215
|
+
index,
|
|
216
|
+
persistedColor: areaLookup.get(id)?.color,
|
|
217
|
+
})
|
|
218
|
+
);
|
|
219
|
+
}, [areaLookup, xpMap]);
|
|
220
|
+
|
|
221
|
+
const skillDistribution = useMemo(() => {
|
|
222
|
+
if (!xpMap) return [];
|
|
223
|
+
return buildDistributionItems(
|
|
224
|
+
aggregateSkills(xpMap.segments),
|
|
225
|
+
xpMap.totalXp,
|
|
226
|
+
(id) => skillLookup.get(id)?.name ?? `Skill ${id}`,
|
|
227
|
+
(id, index) => resolveXpSkillColor({ skillId: id, index })
|
|
228
|
+
);
|
|
229
|
+
}, [skillLookup, xpMap]);
|
|
230
|
+
|
|
231
|
+
const learningTypeDistribution = useMemo(() => {
|
|
232
|
+
if (!xpMap) return [];
|
|
233
|
+
const totals = aggregateLearningTypes(xpMap.segments);
|
|
234
|
+
return [...totals.entries()].map(([id, item], index) => ({
|
|
235
|
+
id,
|
|
236
|
+
label: item.learningType.name,
|
|
237
|
+
xp: item.xp,
|
|
238
|
+
percent:
|
|
239
|
+
xpMap.totalXp > 0 ? Math.round((item.xp / xpMap.totalXp) * 100) : 0,
|
|
240
|
+
color: resolveXpLearningTypeColor({ learningTypeId: id, index }),
|
|
241
|
+
}));
|
|
242
|
+
}, [xpMap]);
|
|
243
|
+
|
|
244
|
+
const segmentViews = useMemo<SegmentView[]>(() => {
|
|
245
|
+
if (!xpMap) return [];
|
|
246
|
+
|
|
247
|
+
return xpMap.segments.map((segment, index) => {
|
|
248
|
+
const primaryArea = pickDominantItem(segment.areas);
|
|
249
|
+
const primarySkill = pickDominantItem(segment.skills);
|
|
250
|
+
const primaryLearningType = pickDominantItem(segment.learningTypes);
|
|
251
|
+
|
|
252
|
+
const areaId = primaryArea?.xpAreaId ?? null;
|
|
253
|
+
const skillId = primarySkill?.xpSkillId ?? null;
|
|
254
|
+
const learningTypeId = primaryLearningType?.xpLearningTypeId ?? null;
|
|
255
|
+
const areaBreakdown = segment.areas.map((area, areaIndex) => ({
|
|
256
|
+
key: area.xpAreaId,
|
|
257
|
+
label: areaLookup.get(area.xpAreaId)?.name ?? `Área ${area.xpAreaId}`,
|
|
258
|
+
percent: area.weightPercent,
|
|
259
|
+
color: resolveXpAreaColor({
|
|
260
|
+
areaId: area.xpAreaId,
|
|
261
|
+
index: areaIndex,
|
|
262
|
+
persistedColor: areaLookup.get(area.xpAreaId)?.color,
|
|
263
|
+
}),
|
|
264
|
+
}));
|
|
265
|
+
const skillBreakdown = segment.skills.map((skill, skillIndex) => ({
|
|
266
|
+
key: skill.xpSkillId,
|
|
267
|
+
label:
|
|
268
|
+
skillLookup.get(skill.xpSkillId)?.name ?? `Skill ${skill.xpSkillId}`,
|
|
269
|
+
percent: skill.weightPercent,
|
|
270
|
+
color: resolveXpSkillColor({
|
|
271
|
+
skillId: skill.xpSkillId,
|
|
272
|
+
index: skillIndex,
|
|
273
|
+
}),
|
|
274
|
+
}));
|
|
275
|
+
const learningTypeBreakdown = segment.learningTypes.map(
|
|
276
|
+
(learningType, learningTypeIndex) => ({
|
|
277
|
+
key: learningType.xpLearningTypeId,
|
|
278
|
+
label: learningType.name ?? `Tipo ${learningType.xpLearningTypeId}`,
|
|
279
|
+
percent: learningType.weightPercent,
|
|
280
|
+
color: resolveXpLearningTypeColor({
|
|
281
|
+
learningTypeId: learningType.xpLearningTypeId,
|
|
282
|
+
learningTypeSlug: learningType.slug,
|
|
283
|
+
index: learningTypeIndex,
|
|
284
|
+
}),
|
|
285
|
+
})
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
return {
|
|
289
|
+
id: segment.id,
|
|
290
|
+
label: `Segmento ${index + 1}`,
|
|
291
|
+
durationSeconds: Math.max(segment.endSeconds - segment.startSeconds, 1),
|
|
292
|
+
startSeconds: segment.startSeconds,
|
|
293
|
+
endSeconds: segment.endSeconds,
|
|
294
|
+
xpValue: segment.xpValue,
|
|
295
|
+
difficulty: segment.difficulty,
|
|
296
|
+
shouldGrantXp: segment.shouldGrantXp,
|
|
297
|
+
aiSummary: segment.aiSummary,
|
|
298
|
+
aiConfidence: segment.aiConfidence,
|
|
299
|
+
areaLabel:
|
|
300
|
+
areaId !== null
|
|
301
|
+
? (areaLookup.get(areaId)?.name ?? `Área ${areaId}`)
|
|
302
|
+
: 'Sem área',
|
|
303
|
+
areaColor:
|
|
304
|
+
areaId !== null
|
|
305
|
+
? resolveXpAreaColor({
|
|
306
|
+
areaId,
|
|
307
|
+
index,
|
|
308
|
+
persistedColor: areaLookup.get(areaId)?.color,
|
|
309
|
+
})
|
|
310
|
+
: '#64748b',
|
|
311
|
+
skillLabel:
|
|
312
|
+
skillId !== null
|
|
313
|
+
? (skillLookup.get(skillId)?.name ?? `Skill ${skillId}`)
|
|
314
|
+
: 'Sem skill',
|
|
315
|
+
skillColor:
|
|
316
|
+
skillId !== null
|
|
317
|
+
? resolveXpSkillColor({ skillId, index })
|
|
318
|
+
: '#64748b',
|
|
319
|
+
learningTypeLabel:
|
|
320
|
+
learningTypeId !== null
|
|
321
|
+
? (primaryLearningType?.name ?? `Tipo ${learningTypeId}`)
|
|
322
|
+
: 'Sem tipo',
|
|
323
|
+
learningTypeColor:
|
|
324
|
+
learningTypeId !== null
|
|
325
|
+
? resolveXpLearningTypeColor({ learningTypeId, index })
|
|
326
|
+
: '#64748b',
|
|
327
|
+
areaBreakdown,
|
|
328
|
+
skillBreakdown,
|
|
329
|
+
learningTypeBreakdown,
|
|
330
|
+
areas: segment.areas,
|
|
331
|
+
skills: segment.skills,
|
|
332
|
+
learningTypes: segment.learningTypes,
|
|
333
|
+
};
|
|
334
|
+
});
|
|
335
|
+
}, [areaLookup, skillLookup, xpMap]);
|
|
336
|
+
|
|
337
|
+
if (isLoading) {
|
|
338
|
+
return <LessonXpSkeleton />;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (!hasTranscription && !xpMap) {
|
|
342
|
+
return (
|
|
343
|
+
<div className="flex flex-col items-center gap-3 px-4 py-12 text-center">
|
|
344
|
+
<BookOpen className="size-8 text-muted-foreground/40" />
|
|
345
|
+
<p className="text-sm text-muted-foreground">
|
|
346
|
+
Esta aula ainda não possui transcrição. Gere ou importe a transcrição
|
|
347
|
+
para calcular o XP com IA.
|
|
348
|
+
</p>
|
|
349
|
+
</div>
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return (
|
|
354
|
+
<div className="flex flex-col gap-2.5">
|
|
355
|
+
<div className="rounded-2xl border border-border/70 bg-linear-to-br from-background via-background to-muted/30 px-3 py-2.5 shadow-[0_14px_34px_-28px_rgba(15,23,42,0.45)] backdrop-blur-sm sm:px-4">
|
|
356
|
+
<div className="flex flex-wrap items-start justify-between gap-3">
|
|
357
|
+
<div className="flex min-w-0 flex-col gap-2">
|
|
358
|
+
<div className="flex min-w-0 items-center gap-1.5">
|
|
359
|
+
{xpMap ? (
|
|
360
|
+
<>
|
|
361
|
+
<Zap className="size-3.5 shrink-0 text-yellow-500" />
|
|
362
|
+
<span className="text-sm font-semibold">
|
|
363
|
+
{xpMap.totalXp} XP
|
|
364
|
+
</span>
|
|
365
|
+
<XpStatusBadge status={xpMap.status} />
|
|
366
|
+
</>
|
|
367
|
+
) : (
|
|
368
|
+
<span className="text-xs text-muted-foreground">
|
|
369
|
+
Sem mapa de XP
|
|
370
|
+
</span>
|
|
371
|
+
)}
|
|
372
|
+
</div>
|
|
373
|
+
{xpMap && (
|
|
374
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
375
|
+
<MetricPill
|
|
376
|
+
label="Segmentos"
|
|
377
|
+
value={String(xpMap.segments.length)}
|
|
378
|
+
/>
|
|
379
|
+
<MetricPill
|
|
380
|
+
label="Áreas"
|
|
381
|
+
value={String(countUniqueAreas(xpMap.segments))}
|
|
382
|
+
tone="sky"
|
|
383
|
+
/>
|
|
384
|
+
<MetricPill
|
|
385
|
+
label="Skills"
|
|
386
|
+
value={String(countUniqueSkills(xpMap.segments))}
|
|
387
|
+
tone="emerald"
|
|
388
|
+
/>
|
|
389
|
+
{avgConfidence(xpMap.segments) !== null && (
|
|
390
|
+
<MetricPill
|
|
391
|
+
label="Confiança"
|
|
392
|
+
value={`${Math.round(avgConfidence(xpMap.segments)! * 100)}%`}
|
|
393
|
+
tone="violet"
|
|
394
|
+
/>
|
|
395
|
+
)}
|
|
396
|
+
</div>
|
|
397
|
+
)}
|
|
398
|
+
</div>
|
|
399
|
+
<div className="flex shrink-0 flex-col items-end gap-0.5">
|
|
400
|
+
<Button
|
|
401
|
+
size="sm"
|
|
402
|
+
variant="outline"
|
|
403
|
+
onClick={handleRecalculate}
|
|
404
|
+
disabled={isRecalculating || isProcessing}
|
|
405
|
+
className="h-8 rounded-xl border-border/70 bg-background/80 gap-1.5 px-3 text-xs shadow-sm"
|
|
406
|
+
>
|
|
407
|
+
{isRecalculating || isProcessing ? (
|
|
408
|
+
<Loader2 className="size-3 animate-spin" />
|
|
409
|
+
) : (
|
|
410
|
+
<RefreshCw className="size-3" />
|
|
411
|
+
)}
|
|
412
|
+
Recalcular XP
|
|
413
|
+
</Button>
|
|
414
|
+
{displayError && (
|
|
415
|
+
<p className="max-w-40 text-right text-[0.65rem] leading-tight text-destructive">
|
|
416
|
+
{displayError}
|
|
417
|
+
</p>
|
|
418
|
+
)}
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
</div>
|
|
422
|
+
|
|
423
|
+
{isProcessing && (
|
|
424
|
+
<div className="flex items-center gap-2 rounded-md border border-yellow-500/30 bg-yellow-500/10 px-2.5 py-1.5 text-xs text-yellow-700 dark:text-yellow-400">
|
|
425
|
+
<Loader2 className="size-3.5 shrink-0 animate-spin" />
|
|
426
|
+
Calculando XP com IA...
|
|
427
|
+
</div>
|
|
428
|
+
)}
|
|
429
|
+
|
|
430
|
+
{xpMap && (
|
|
431
|
+
<div className="grid gap-3 xl:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
|
|
432
|
+
<DistributionCard
|
|
433
|
+
title="Áreas Macro"
|
|
434
|
+
description="Distribuição do XP por área macro na aula"
|
|
435
|
+
icon={<Cpu className="size-4 text-sky-600" />}
|
|
436
|
+
total={xpMap.totalXp}
|
|
437
|
+
items={areaDistribution}
|
|
438
|
+
toneClassName="bg-linear-to-r from-sky-500/10 via-sky-500/5 to-transparent"
|
|
439
|
+
/>
|
|
440
|
+
<DistributionCard
|
|
441
|
+
title="Skills"
|
|
442
|
+
description="Distribuição do XP por skill principal"
|
|
443
|
+
icon={<Zap className="size-4 text-emerald-600" />}
|
|
444
|
+
total={xpMap.totalXp}
|
|
445
|
+
items={skillDistribution}
|
|
446
|
+
toneClassName="bg-linear-to-r from-emerald-500/10 via-emerald-500/5 to-transparent"
|
|
447
|
+
/>
|
|
448
|
+
<TimelineCard
|
|
449
|
+
segmentViews={segmentViews}
|
|
450
|
+
totalDuration={Math.max(
|
|
451
|
+
...segmentViews.map((segment) => segment.endSeconds),
|
|
452
|
+
0
|
|
453
|
+
)}
|
|
454
|
+
/>
|
|
455
|
+
<LearningTypesCard items={learningTypeDistribution} />
|
|
456
|
+
</div>
|
|
457
|
+
)}
|
|
458
|
+
|
|
459
|
+
{xpMap && (
|
|
460
|
+
<Card className="border-border/70">
|
|
461
|
+
<CardHeader className="pb-1.5">
|
|
462
|
+
<CardTitle className="text-sm font-semibold">
|
|
463
|
+
Resumo do mapa
|
|
464
|
+
</CardTitle>
|
|
465
|
+
</CardHeader>
|
|
466
|
+
<CardContent className="grid grid-cols-2 gap-x-3 gap-y-1.5 pt-0 text-xs sm:grid-cols-4">
|
|
467
|
+
<SummaryItem
|
|
468
|
+
label="Áreas"
|
|
469
|
+
value={String(countUniqueAreas(xpMap.segments))}
|
|
470
|
+
/>
|
|
471
|
+
<SummaryItem
|
|
472
|
+
label="Skills"
|
|
473
|
+
value={String(countUniqueSkills(xpMap.segments))}
|
|
474
|
+
/>
|
|
475
|
+
<SummaryItem
|
|
476
|
+
label="Tipos"
|
|
477
|
+
value={String(countUniqueLearningTypes(xpMap.segments))}
|
|
478
|
+
/>
|
|
479
|
+
<SummaryItem
|
|
480
|
+
label="Segmentos"
|
|
481
|
+
value={String(xpMap.segments.length)}
|
|
482
|
+
/>
|
|
483
|
+
{xpMap.generatedAt && (
|
|
484
|
+
<SummaryItem
|
|
485
|
+
label="Gerado"
|
|
486
|
+
value={formatDate(xpMap.generatedAt)}
|
|
487
|
+
/>
|
|
488
|
+
)}
|
|
489
|
+
{xpMap.aiModelVersion && (
|
|
490
|
+
<SummaryItem label="Modelo" value={xpMap.aiModelVersion} />
|
|
491
|
+
)}
|
|
492
|
+
{avgConfidence(xpMap.segments) !== null && (
|
|
493
|
+
<SummaryItem
|
|
494
|
+
label="Confiança"
|
|
495
|
+
value={`${Math.round(avgConfidence(xpMap.segments)! * 100)}%`}
|
|
496
|
+
/>
|
|
497
|
+
)}
|
|
498
|
+
</CardContent>
|
|
499
|
+
</Card>
|
|
500
|
+
)}
|
|
501
|
+
|
|
502
|
+
{!xpMap && !isProcessing && (
|
|
503
|
+
<div className="flex flex-col items-center gap-2 py-6 text-center">
|
|
504
|
+
<Brain className="size-7 text-muted-foreground/40" />
|
|
505
|
+
<p className="text-xs text-muted-foreground">
|
|
506
|
+
Nenhum mapa de XP calculado.
|
|
507
|
+
</p>
|
|
508
|
+
</div>
|
|
509
|
+
)}
|
|
510
|
+
|
|
511
|
+
{xpMap?.processingError && (
|
|
512
|
+
<div className="flex items-start gap-1.5 rounded-md bg-destructive/10 px-2.5 py-1.5 text-xs text-destructive">
|
|
513
|
+
<AlertCircle className="mt-0.5 size-3.5 shrink-0" />
|
|
514
|
+
<span>{xpMap.processingError}</span>
|
|
515
|
+
</div>
|
|
516
|
+
)}
|
|
517
|
+
|
|
518
|
+
{xpMap && xpMap.segments.length > 0 && !isProcessing && (
|
|
519
|
+
<>
|
|
520
|
+
<SegmentsSection
|
|
521
|
+
segments={segmentViews}
|
|
522
|
+
areaLookup={areaLookup}
|
|
523
|
+
skillLookup={skillLookup}
|
|
524
|
+
locale={currentLocaleCode || 'pt-BR'}
|
|
525
|
+
/>
|
|
526
|
+
</>
|
|
527
|
+
)}
|
|
528
|
+
</div>
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function LessonXpSkeleton() {
|
|
533
|
+
return (
|
|
534
|
+
<div className="flex flex-col gap-3 p-1">
|
|
535
|
+
<div className="flex items-center justify-between gap-2">
|
|
536
|
+
<Skeleton className="h-5 w-24 rounded-full" />
|
|
537
|
+
<Skeleton className="h-7 w-28 rounded-md" />
|
|
538
|
+
</div>
|
|
539
|
+
<div className="grid gap-3 xl:grid-cols-[1fr_1fr]">
|
|
540
|
+
<Skeleton className="h-72 rounded-xl" />
|
|
541
|
+
<Skeleton className="h-72 rounded-xl" />
|
|
542
|
+
<Skeleton className="h-28 rounded-xl xl:col-span-2" />
|
|
543
|
+
</div>
|
|
544
|
+
{Array.from({ length: 3 }).map((_, index) => (
|
|
545
|
+
<div
|
|
546
|
+
key={index}
|
|
547
|
+
className="flex flex-col gap-2 rounded-lg border bg-muted/20 p-3"
|
|
548
|
+
>
|
|
549
|
+
<div className="flex items-center justify-between gap-2">
|
|
550
|
+
<Skeleton className="h-3 w-32" />
|
|
551
|
+
<Skeleton className="h-4 w-14 rounded-full" />
|
|
552
|
+
</div>
|
|
553
|
+
<Skeleton className="h-3 w-3/4" />
|
|
554
|
+
<div className="flex items-center gap-2">
|
|
555
|
+
<Skeleton className="h-3 w-16" />
|
|
556
|
+
<Skeleton className="h-3 w-20" />
|
|
557
|
+
<Skeleton className="h-3 w-12" />
|
|
558
|
+
</div>
|
|
559
|
+
</div>
|
|
560
|
+
))}
|
|
561
|
+
</div>
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
function DistributionCard({
|
|
566
|
+
title,
|
|
567
|
+
description,
|
|
568
|
+
icon,
|
|
569
|
+
total,
|
|
570
|
+
items,
|
|
571
|
+
toneClassName,
|
|
572
|
+
}: {
|
|
573
|
+
title: string;
|
|
574
|
+
description: string;
|
|
575
|
+
icon: ReactNode;
|
|
576
|
+
total: number;
|
|
577
|
+
items: DistributionItem[];
|
|
578
|
+
toneClassName: string;
|
|
579
|
+
}) {
|
|
580
|
+
if (!items.length) return null;
|
|
581
|
+
|
|
582
|
+
return (
|
|
583
|
+
<Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)]">
|
|
584
|
+
<CardHeader
|
|
585
|
+
className={cn(
|
|
586
|
+
'space-y-0.5 border-b border-border/60 pt-3 pb-1.5!',
|
|
587
|
+
toneClassName
|
|
588
|
+
)}
|
|
589
|
+
>
|
|
590
|
+
<div className="flex items-center justify-between gap-3">
|
|
591
|
+
<div className="min-w-0">
|
|
592
|
+
<CardTitle className="text-sm font-semibold">{title}</CardTitle>
|
|
593
|
+
<p className="text-xs text-muted-foreground">{description}</p>
|
|
594
|
+
</div>
|
|
595
|
+
<div className="flex items-center gap-2 rounded-full border border-white/40 bg-background/85 px-2 py-1 shadow-sm backdrop-blur-sm transition-all duration-200 hover:-translate-y-0.5 hover:bg-background hover:shadow-md dark:border-white/10 dark:bg-background/70">
|
|
596
|
+
{icon}
|
|
597
|
+
<span className="text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground">
|
|
598
|
+
{items.length} itens
|
|
599
|
+
</span>
|
|
600
|
+
</div>
|
|
601
|
+
</div>
|
|
602
|
+
</CardHeader>
|
|
603
|
+
<CardContent className="flex flex-col gap-2.5 px-2.5 py-2.5 sm:px-3">
|
|
604
|
+
<div className="relative h-44">
|
|
605
|
+
<ResponsiveContainer width="100%" height="100%">
|
|
606
|
+
<PieChart>
|
|
607
|
+
<Pie
|
|
608
|
+
data={items}
|
|
609
|
+
cx="50%"
|
|
610
|
+
cy="50%"
|
|
611
|
+
innerRadius={46}
|
|
612
|
+
outerRadius={68}
|
|
613
|
+
paddingAngle={2.5}
|
|
614
|
+
dataKey="xp"
|
|
615
|
+
strokeWidth={3}
|
|
616
|
+
stroke="hsl(var(--background))"
|
|
617
|
+
>
|
|
618
|
+
{items.map((entry) => (
|
|
619
|
+
<Cell key={entry.id} fill={entry.color} />
|
|
620
|
+
))}
|
|
621
|
+
</Pie>
|
|
622
|
+
<RechartsTooltip
|
|
623
|
+
wrapperStyle={chartTooltipWrapperStyle}
|
|
624
|
+
content={<DistributionTooltip />}
|
|
625
|
+
/>
|
|
626
|
+
</PieChart>
|
|
627
|
+
</ResponsiveContainer>
|
|
628
|
+
<div className="pointer-events-none absolute inset-0 flex flex-col items-center justify-center">
|
|
629
|
+
<span className="text-xl font-bold text-foreground">{total}</span>
|
|
630
|
+
<span className="text-[10px] text-muted-foreground">XP total</span>
|
|
631
|
+
</div>
|
|
632
|
+
</div>
|
|
633
|
+
|
|
634
|
+
<div className="grid max-h-40 grid-cols-[repeat(auto-fit,minmax(220px,1fr))] gap-1.5 overflow-auto pr-1">
|
|
635
|
+
{items.map((item) => (
|
|
636
|
+
<div
|
|
637
|
+
key={item.id}
|
|
638
|
+
className="flex min-w-0 items-start gap-2 rounded-xl border border-border/50 bg-muted/15 px-2 py-1.5 transition-all duration-200 hover:-translate-y-0.5 hover:border-border hover:bg-background/90 hover:shadow-md"
|
|
639
|
+
>
|
|
640
|
+
<span
|
|
641
|
+
className="mt-1 size-2 shrink-0 rounded-sm"
|
|
642
|
+
style={{ backgroundColor: item.color }}
|
|
643
|
+
/>
|
|
644
|
+
<div className="flex min-w-0 flex-col">
|
|
645
|
+
<span className="wrap-break-word text-xs font-medium leading-tight text-foreground">
|
|
646
|
+
{item.label}
|
|
647
|
+
</span>
|
|
648
|
+
<span className="text-[10px] text-muted-foreground">
|
|
649
|
+
{item.xp} XP ({item.percent}%)
|
|
650
|
+
</span>
|
|
651
|
+
</div>
|
|
652
|
+
</div>
|
|
653
|
+
))}
|
|
654
|
+
</div>
|
|
655
|
+
</CardContent>
|
|
656
|
+
</Card>
|
|
657
|
+
);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function TimelineCard({
|
|
661
|
+
segmentViews,
|
|
662
|
+
totalDuration,
|
|
663
|
+
}: {
|
|
664
|
+
segmentViews: SegmentView[];
|
|
665
|
+
totalDuration: number;
|
|
666
|
+
}) {
|
|
667
|
+
if (!segmentViews.length) return null;
|
|
668
|
+
|
|
669
|
+
const safeDuration = Math.max(totalDuration, 1);
|
|
670
|
+
|
|
671
|
+
return (
|
|
672
|
+
<Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)] xl:col-span-2">
|
|
673
|
+
<CardHeader className="border-b border-border/60 bg-linear-to-r from-slate-500/10 via-slate-500/5 to-transparent pt-3 pb-1.5!">
|
|
674
|
+
<div className="flex items-center justify-between gap-3">
|
|
675
|
+
<div>
|
|
676
|
+
<CardTitle className="text-sm font-semibold">
|
|
677
|
+
Timeline da aula
|
|
678
|
+
</CardTitle>
|
|
679
|
+
<p className="text-xs text-muted-foreground">
|
|
680
|
+
Segmentos proporcionais ao tempo total, com pesos visuais por
|
|
681
|
+
área, skill e tipo
|
|
682
|
+
</p>
|
|
683
|
+
</div>
|
|
684
|
+
<div className="flex items-center gap-2 rounded-full border border-white/40 bg-background/85 px-2 py-1 shadow-sm backdrop-blur-sm transition-all duration-200 hover:-translate-y-0.5 hover:bg-background hover:shadow-md dark:border-white/10 dark:bg-background/70">
|
|
685
|
+
<Clock3 className="size-4 text-muted-foreground" />
|
|
686
|
+
<span className="text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground">
|
|
687
|
+
{segmentViews.length} blocos
|
|
688
|
+
</span>
|
|
689
|
+
</div>
|
|
690
|
+
</div>
|
|
691
|
+
</CardHeader>
|
|
692
|
+
<CardContent className="px-2.5 py-2.5 sm:px-3">
|
|
693
|
+
<div className="rounded-xl border border-border/60 bg-linear-to-br from-muted/35 via-muted/20 to-background p-2.5">
|
|
694
|
+
<TooltipProvider delayDuration={120}>
|
|
695
|
+
<div className="flex h-12 items-stretch gap-1 overflow-x-auto rounded-lg">
|
|
696
|
+
{segmentViews.map((segment) => {
|
|
697
|
+
const width = Math.max(
|
|
698
|
+
4,
|
|
699
|
+
(segment.durationSeconds / safeDuration) * 100
|
|
700
|
+
);
|
|
701
|
+
return (
|
|
702
|
+
<Tooltip key={segment.id}>
|
|
703
|
+
<TooltipTrigger asChild>
|
|
704
|
+
<button
|
|
705
|
+
type="button"
|
|
706
|
+
className={cn(
|
|
707
|
+
'group relative flex min-w-0 flex-col overflow-hidden rounded-md border border-border/60 bg-background/80 text-left outline-none transition-all duration-200 hover:-translate-y-0.5 hover:border-border hover:bg-background focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1',
|
|
708
|
+
!segment.shouldGrantXp && 'opacity-50'
|
|
709
|
+
)}
|
|
710
|
+
style={{ flexBasis: `${width}%`, flexGrow: 0 }}
|
|
711
|
+
aria-label={`${segment.label} ${formatSeconds(segment.startSeconds)} a ${formatSeconds(segment.endSeconds)} ${segment.xpValue} XP`}
|
|
712
|
+
>
|
|
713
|
+
<SegmentCompositionRow
|
|
714
|
+
items={segment.areaBreakdown}
|
|
715
|
+
getKey={(item) => item.key}
|
|
716
|
+
getValue={(item) => item.percent}
|
|
717
|
+
getColor={(item) => item.color}
|
|
718
|
+
/>
|
|
719
|
+
<SegmentCompositionRow
|
|
720
|
+
items={segment.skillBreakdown}
|
|
721
|
+
getKey={(item) => item.key}
|
|
722
|
+
getValue={(item) => item.percent}
|
|
723
|
+
getColor={(item) => item.color}
|
|
724
|
+
/>
|
|
725
|
+
<SegmentCompositionRow
|
|
726
|
+
items={segment.learningTypeBreakdown}
|
|
727
|
+
getKey={(item) => item.key}
|
|
728
|
+
getValue={(item) => item.percent}
|
|
729
|
+
getColor={(item) => item.color}
|
|
730
|
+
/>
|
|
731
|
+
<div className="pointer-events-none absolute inset-x-1 bottom-0.5 flex items-center justify-between gap-1 rounded bg-black/45 px-1 py-0.5 text-[9px] font-medium text-white opacity-0 transition-opacity group-hover:opacity-100 group-focus-visible:opacity-100">
|
|
732
|
+
<span>{formatSeconds(segment.startSeconds)}</span>
|
|
733
|
+
<span>{segment.xpValue} XP</span>
|
|
734
|
+
</div>
|
|
735
|
+
</button>
|
|
736
|
+
</TooltipTrigger>
|
|
737
|
+
<TooltipContent
|
|
738
|
+
side="top"
|
|
739
|
+
hideArrow
|
|
740
|
+
className="max-w-72 rounded-lg border border-border bg-popover px-3 py-2 text-popover-foreground shadow-lg"
|
|
741
|
+
>
|
|
742
|
+
<SegmentTooltipContent segment={segment} />
|
|
743
|
+
</TooltipContent>
|
|
744
|
+
</Tooltip>
|
|
745
|
+
);
|
|
746
|
+
})}
|
|
747
|
+
</div>
|
|
748
|
+
</TooltipProvider>
|
|
749
|
+
<div className="mt-2 flex items-center justify-between text-[10px] text-muted-foreground">
|
|
750
|
+
<span>{formatSeconds(0)}</span>
|
|
751
|
+
<span>{formatSeconds(safeDuration)}</span>
|
|
752
|
+
</div>
|
|
753
|
+
</div>
|
|
754
|
+
</CardContent>
|
|
755
|
+
</Card>
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
function LearningTypesCard({ items }: { items: DistributionItem[] }) {
|
|
760
|
+
if (!items.length) return null;
|
|
761
|
+
|
|
762
|
+
return (
|
|
763
|
+
<Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)] xl:col-span-2">
|
|
764
|
+
<CardHeader className="border-b border-border/60 bg-linear-to-r from-violet-500/10 via-violet-500/5 to-transparent pt-3 pb-1.5!">
|
|
765
|
+
<div className="flex items-center justify-between gap-3">
|
|
766
|
+
<CardTitle className="text-sm font-semibold">
|
|
767
|
+
Tipos de aprendizado
|
|
768
|
+
</CardTitle>
|
|
769
|
+
<div className="rounded-full border border-white/40 bg-background/85 px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground shadow-sm backdrop-blur-sm transition-all duration-200 hover:-translate-y-0.5 hover:bg-background hover:shadow-md dark:border-white/10 dark:bg-background/70">
|
|
770
|
+
{items.length} tipos
|
|
771
|
+
</div>
|
|
772
|
+
</div>
|
|
773
|
+
</CardHeader>
|
|
774
|
+
<CardContent className="grid grid-cols-[repeat(auto-fit,minmax(220px,1fr))] content-start gap-2 px-2.5 py-2.5 sm:px-3">
|
|
775
|
+
{items.map((item) => (
|
|
776
|
+
<div
|
|
777
|
+
key={item.id}
|
|
778
|
+
className="flex h-full flex-col justify-start gap-1.5 rounded-xl border border-border/60 bg-muted/20 px-2.5 py-2 text-xs shadow-sm transition-all duration-200 hover:-translate-y-0.5 hover:border-violet-500/20 hover:bg-background/95 hover:shadow-md"
|
|
779
|
+
>
|
|
780
|
+
<div className="flex flex-wrap items-center justify-between gap-2">
|
|
781
|
+
<span className="min-w-0 flex-1 font-medium">{item.label}</span>
|
|
782
|
+
<span className="font-semibold tabular-nums">{item.xp} XP</span>
|
|
783
|
+
</div>
|
|
784
|
+
<div className="h-1.5 overflow-hidden rounded-full bg-muted">
|
|
785
|
+
<div
|
|
786
|
+
className="h-full rounded-full"
|
|
787
|
+
style={{
|
|
788
|
+
width: `${item.percent}%`,
|
|
789
|
+
backgroundColor: item.color,
|
|
790
|
+
}}
|
|
791
|
+
/>
|
|
792
|
+
</div>
|
|
793
|
+
<div className="text-[10px] text-muted-foreground">
|
|
794
|
+
{item.percent}%
|
|
795
|
+
</div>
|
|
796
|
+
</div>
|
|
797
|
+
))}
|
|
798
|
+
</CardContent>
|
|
799
|
+
</Card>
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function SegmentsSection({
|
|
804
|
+
segments,
|
|
805
|
+
areaLookup,
|
|
806
|
+
skillLookup,
|
|
807
|
+
locale,
|
|
808
|
+
}: {
|
|
809
|
+
segments: SegmentView[];
|
|
810
|
+
areaLookup: Map<number, XpAreaOption>;
|
|
811
|
+
skillLookup: Map<number, XpSkillOption>;
|
|
812
|
+
locale: string;
|
|
813
|
+
}) {
|
|
814
|
+
return (
|
|
815
|
+
<Card className="min-w-0 overflow-hidden border-border/70 bg-card/95 py-0 gap-0! shadow-[0_18px_40px_-32px_rgba(15,23,42,0.45)]">
|
|
816
|
+
<CardHeader className="border-b border-border/60 bg-linear-to-r from-cyan-500/10 via-cyan-500/5 to-transparent pt-3 pb-1.5!">
|
|
817
|
+
<div className="flex items-center justify-between gap-3">
|
|
818
|
+
<CardTitle className="text-sm font-semibold">
|
|
819
|
+
Segmentos ({segments.length})
|
|
820
|
+
</CardTitle>
|
|
821
|
+
<div className="rounded-full border border-white/40 bg-background/85 px-2 py-1 text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground shadow-sm backdrop-blur-sm transition-all duration-200 hover:-translate-y-0.5 hover:bg-background hover:shadow-md dark:border-white/10 dark:bg-background/70">
|
|
822
|
+
leitura IA
|
|
823
|
+
</div>
|
|
824
|
+
</div>
|
|
825
|
+
</CardHeader>
|
|
826
|
+
<CardContent className="flex flex-col gap-2.5 px-2.5 py-2.5 sm:px-3">
|
|
827
|
+
<TooltipProvider delayDuration={120}>
|
|
828
|
+
{segments.map((segment) => (
|
|
829
|
+
<div
|
|
830
|
+
key={segment.id}
|
|
831
|
+
className={cn(
|
|
832
|
+
'flex flex-col gap-2 rounded-2xl border border-border/60 bg-linear-to-br from-background via-background to-muted/25 px-3 py-2.5 text-xs shadow-[0_16px_30px_-24px_rgba(15,23,42,0.45)] transition-all duration-200 hover:-translate-y-0.5 hover:border-cyan-500/15 hover:to-muted/35 hover:shadow-[0_20px_36px_-24px_rgba(15,23,42,0.52)]',
|
|
833
|
+
!segment.shouldGrantXp && 'opacity-50'
|
|
834
|
+
)}
|
|
835
|
+
>
|
|
836
|
+
<div className="flex flex-wrap items-start justify-between gap-2">
|
|
837
|
+
<div className="flex min-w-0 flex-col gap-1">
|
|
838
|
+
<div className="flex flex-wrap items-center gap-1.5">
|
|
839
|
+
<span className="rounded-full border border-cyan-500/15 bg-cyan-500/10 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-cyan-700 transition-all duration-200 hover:bg-cyan-500/15 hover:shadow-sm dark:text-cyan-300">
|
|
840
|
+
{segment.label}
|
|
841
|
+
</span>
|
|
842
|
+
<span className="rounded-full border border-border/60 bg-background/90 px-2 py-0.5 font-mono text-[10px] text-muted-foreground shadow-sm transition-all duration-200 hover:border-border hover:bg-background hover:text-foreground hover:shadow-md">
|
|
843
|
+
{formatSeconds(segment.startSeconds)} -{' '}
|
|
844
|
+
{formatSeconds(segment.endSeconds)}
|
|
845
|
+
</span>
|
|
846
|
+
</div>
|
|
847
|
+
<div className="flex flex-wrap items-center gap-1.5 text-[11px] text-muted-foreground">
|
|
848
|
+
<span className="font-medium text-foreground">
|
|
849
|
+
{segment.areaLabel}
|
|
850
|
+
</span>
|
|
851
|
+
<span className="text-muted-foreground/60">/</span>
|
|
852
|
+
<span>{segment.skillLabel}</span>
|
|
853
|
+
<span className="text-muted-foreground/60">/</span>
|
|
854
|
+
<span>{segment.learningTypeLabel}</span>
|
|
855
|
+
</div>
|
|
856
|
+
</div>
|
|
857
|
+
|
|
858
|
+
<div className="flex flex-wrap items-center justify-end gap-1.5">
|
|
859
|
+
<span className="rounded-full border border-amber-500/15 bg-amber-500/10 px-2 py-0.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-amber-700 dark:text-amber-300">
|
|
860
|
+
{segment.xpValue} XP
|
|
861
|
+
</span>
|
|
862
|
+
<DifficultyBadge difficulty={segment.difficulty} />
|
|
863
|
+
{segment.aiConfidence !== null && (
|
|
864
|
+
<span className="rounded-full border border-border/60 bg-background/90 px-2 py-0.5 text-[10px] font-semibold text-muted-foreground shadow-sm">
|
|
865
|
+
{Math.round(segment.aiConfidence * 100)}% conf.
|
|
866
|
+
</span>
|
|
867
|
+
)}
|
|
868
|
+
</div>
|
|
869
|
+
</div>
|
|
870
|
+
|
|
871
|
+
<div className="overflow-hidden rounded-full border border-border/50 bg-muted/40">
|
|
872
|
+
<div className="flex h-1.5 w-full overflow-hidden">
|
|
873
|
+
{buildSegmentComposition(
|
|
874
|
+
segment.areaBreakdown,
|
|
875
|
+
(item) => item.percent
|
|
876
|
+
).map(({ item, percent }, index) => (
|
|
877
|
+
<Tooltip key={`area-${item.key}-${index}`}>
|
|
878
|
+
<TooltipTrigger asChild>
|
|
879
|
+
<div
|
|
880
|
+
className="h-full cursor-help transition-opacity hover:opacity-85"
|
|
881
|
+
style={{
|
|
882
|
+
width: `${percent}%`,
|
|
883
|
+
backgroundColor: item.color,
|
|
884
|
+
}}
|
|
885
|
+
/>
|
|
886
|
+
</TooltipTrigger>
|
|
887
|
+
<TooltipContent
|
|
888
|
+
side="top"
|
|
889
|
+
hideArrow
|
|
890
|
+
className="rounded-lg border border-border bg-popover px-2.5 py-1.5 text-xs text-popover-foreground shadow-lg"
|
|
891
|
+
>
|
|
892
|
+
<div className="flex items-center gap-1.5">
|
|
893
|
+
<span
|
|
894
|
+
className="size-2 rounded-full"
|
|
895
|
+
style={{ backgroundColor: item.color }}
|
|
896
|
+
/>
|
|
897
|
+
<span className="font-medium text-foreground">
|
|
898
|
+
{item.label}
|
|
899
|
+
</span>
|
|
900
|
+
<span className="text-muted-foreground">
|
|
901
|
+
{Math.round(percent)}%
|
|
902
|
+
</span>
|
|
903
|
+
</div>
|
|
904
|
+
</TooltipContent>
|
|
905
|
+
</Tooltip>
|
|
906
|
+
))}
|
|
907
|
+
</div>
|
|
908
|
+
</div>
|
|
909
|
+
|
|
910
|
+
{segment.aiSummary && (
|
|
911
|
+
<div className="rounded-xl border border-border/50 bg-muted/20 px-2.5 py-2">
|
|
912
|
+
<p className="line-clamp-2 leading-relaxed text-muted-foreground">
|
|
913
|
+
{segment.aiSummary}
|
|
914
|
+
</p>
|
|
915
|
+
</div>
|
|
916
|
+
)}
|
|
917
|
+
<div className="grid gap-1.5 md:grid-cols-2 xl:grid-cols-3">
|
|
918
|
+
<SegmentBreakdownGroup
|
|
919
|
+
title="Áreas"
|
|
920
|
+
tone="sky"
|
|
921
|
+
items={segment.areaBreakdown.map((area) => ({
|
|
922
|
+
key: area.key,
|
|
923
|
+
label:
|
|
924
|
+
areaLookup.get(area.key)?.name ??
|
|
925
|
+
area.label ??
|
|
926
|
+
`Área ${area.key}`,
|
|
927
|
+
percent: area.percent,
|
|
928
|
+
color: area.color,
|
|
929
|
+
}))}
|
|
930
|
+
/>
|
|
931
|
+
<SegmentBreakdownGroup
|
|
932
|
+
title="Skills"
|
|
933
|
+
tone="emerald"
|
|
934
|
+
items={segment.skillBreakdown.map((skill) => ({
|
|
935
|
+
key: skill.key,
|
|
936
|
+
label:
|
|
937
|
+
skillLookup.get(skill.key)?.name ??
|
|
938
|
+
skill.label ??
|
|
939
|
+
`Skill ${skill.key}`,
|
|
940
|
+
percent: skill.percent,
|
|
941
|
+
color: skill.color,
|
|
942
|
+
}))}
|
|
943
|
+
/>
|
|
944
|
+
<SegmentBreakdownGroup
|
|
945
|
+
title="Tipos"
|
|
946
|
+
tone="violet"
|
|
947
|
+
items={segment.learningTypeBreakdown.map((learningType) => {
|
|
948
|
+
const metadata = segment.learningTypes.find(
|
|
949
|
+
(item) => item.xpLearningTypeId === learningType.key
|
|
950
|
+
);
|
|
951
|
+
|
|
952
|
+
return {
|
|
953
|
+
key: learningType.key,
|
|
954
|
+
label:
|
|
955
|
+
learningType.label +
|
|
956
|
+
(metadata?.multiplier !== undefined
|
|
957
|
+
? ` ${formatMultiplier(metadata.multiplier, locale)}`
|
|
958
|
+
: ''),
|
|
959
|
+
percent: learningType.percent,
|
|
960
|
+
color: learningType.color,
|
|
961
|
+
};
|
|
962
|
+
})}
|
|
963
|
+
/>
|
|
964
|
+
</div>
|
|
965
|
+
</div>
|
|
966
|
+
))}
|
|
967
|
+
</TooltipProvider>
|
|
968
|
+
</CardContent>
|
|
969
|
+
</Card>
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
function DistributionTooltip({
|
|
974
|
+
active,
|
|
975
|
+
payload,
|
|
976
|
+
}: {
|
|
977
|
+
active?: boolean;
|
|
978
|
+
payload?: Array<{
|
|
979
|
+
name?: string;
|
|
980
|
+
value?: number;
|
|
981
|
+
payload?: { color?: string; label?: string; percent?: number };
|
|
982
|
+
}>;
|
|
983
|
+
}) {
|
|
984
|
+
if (!active || !payload?.length) return null;
|
|
985
|
+
|
|
986
|
+
const entry = payload[0];
|
|
987
|
+
const label = entry?.payload?.label ?? entry?.name ?? 'Item';
|
|
988
|
+
const percent = entry?.payload?.percent;
|
|
989
|
+
|
|
990
|
+
return (
|
|
991
|
+
<div
|
|
992
|
+
className="rounded-xl border border-border bg-white px-3 py-2 text-slate-900 shadow-xl"
|
|
993
|
+
style={chartTooltipStyle as CSSProperties}
|
|
994
|
+
>
|
|
995
|
+
<p className="flex items-center gap-1.5 text-xs text-slate-500">
|
|
996
|
+
<span
|
|
997
|
+
className="inline-block size-2 rounded-full"
|
|
998
|
+
style={{ backgroundColor: entry?.payload?.color }}
|
|
999
|
+
/>
|
|
1000
|
+
{label}
|
|
1001
|
+
</p>
|
|
1002
|
+
<p className="mt-0.5 text-sm font-semibold text-slate-900">
|
|
1003
|
+
{entry?.value} XP
|
|
1004
|
+
</p>
|
|
1005
|
+
{typeof percent === 'number' && (
|
|
1006
|
+
<p className="mt-0.5 text-[11px] text-slate-500">{percent}% do total</p>
|
|
1007
|
+
)}
|
|
1008
|
+
</div>
|
|
1009
|
+
);
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
function SummaryItem({ label, value }: { label: string; value: string }) {
|
|
1013
|
+
return (
|
|
1014
|
+
<div className="rounded-xl border border-border/50 bg-muted/15 px-2 py-1.5">
|
|
1015
|
+
<div className="text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground">
|
|
1016
|
+
{label}
|
|
1017
|
+
</div>
|
|
1018
|
+
<div className="mt-0.5 text-sm font-semibold text-foreground">
|
|
1019
|
+
{value}
|
|
1020
|
+
</div>
|
|
1021
|
+
</div>
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function MetricPill({
|
|
1026
|
+
label,
|
|
1027
|
+
value,
|
|
1028
|
+
tone = 'default',
|
|
1029
|
+
}: {
|
|
1030
|
+
label: string;
|
|
1031
|
+
value: string;
|
|
1032
|
+
tone?: 'default' | 'sky' | 'emerald' | 'violet';
|
|
1033
|
+
}) {
|
|
1034
|
+
const toneClassName =
|
|
1035
|
+
tone === 'sky'
|
|
1036
|
+
? 'border-sky-500/15 bg-sky-500/10 text-sky-700 dark:text-sky-300'
|
|
1037
|
+
: tone === 'emerald'
|
|
1038
|
+
? 'border-emerald-500/15 bg-emerald-500/10 text-emerald-700 dark:text-emerald-300'
|
|
1039
|
+
: tone === 'violet'
|
|
1040
|
+
? 'border-violet-500/15 bg-violet-500/10 text-violet-700 dark:text-violet-300'
|
|
1041
|
+
: 'border-border/60 bg-muted/40 text-foreground';
|
|
1042
|
+
|
|
1043
|
+
return (
|
|
1044
|
+
<div
|
|
1045
|
+
className={cn(
|
|
1046
|
+
'inline-flex items-center gap-2 rounded-full border px-2.5 py-1 text-xs shadow-sm',
|
|
1047
|
+
toneClassName
|
|
1048
|
+
)}
|
|
1049
|
+
>
|
|
1050
|
+
<span className="text-[10px] font-semibold uppercase tracking-[0.14em] opacity-80">
|
|
1051
|
+
{label}
|
|
1052
|
+
</span>
|
|
1053
|
+
<span className="font-semibold">{value}</span>
|
|
1054
|
+
</div>
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function SegmentBreakdownGroup({
|
|
1059
|
+
title,
|
|
1060
|
+
tone,
|
|
1061
|
+
items,
|
|
1062
|
+
}: {
|
|
1063
|
+
title: string;
|
|
1064
|
+
tone: 'sky' | 'emerald' | 'violet';
|
|
1065
|
+
items: Array<{
|
|
1066
|
+
key: number;
|
|
1067
|
+
label: string;
|
|
1068
|
+
percent: number;
|
|
1069
|
+
color: string;
|
|
1070
|
+
}>;
|
|
1071
|
+
}) {
|
|
1072
|
+
const toneClassName =
|
|
1073
|
+
tone === 'sky'
|
|
1074
|
+
? 'border-sky-500/15 bg-sky-500/8'
|
|
1075
|
+
: tone === 'emerald'
|
|
1076
|
+
? 'border-emerald-500/15 bg-emerald-500/8'
|
|
1077
|
+
: 'border-violet-500/15 bg-violet-500/8';
|
|
1078
|
+
|
|
1079
|
+
if (!items.length) {
|
|
1080
|
+
return null;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
return (
|
|
1084
|
+
<div className={cn('rounded-xl border px-2 py-2', toneClassName)}>
|
|
1085
|
+
<div className="mb-1.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-muted-foreground">
|
|
1086
|
+
{title}
|
|
1087
|
+
</div>
|
|
1088
|
+
<div className="flex flex-wrap gap-1">
|
|
1089
|
+
{items.map((item) => (
|
|
1090
|
+
<span
|
|
1091
|
+
key={item.key}
|
|
1092
|
+
className="inline-flex items-center gap-1.5 rounded-full border border-border/50 bg-background/90 px-2 py-0.5 text-[10px] text-foreground shadow-sm"
|
|
1093
|
+
>
|
|
1094
|
+
<span
|
|
1095
|
+
className="size-1.5 rounded-full"
|
|
1096
|
+
style={{ backgroundColor: item.color }}
|
|
1097
|
+
/>
|
|
1098
|
+
<span className="max-w-44 truncate">{item.label}</span>
|
|
1099
|
+
<span className="font-semibold text-muted-foreground">
|
|
1100
|
+
{Math.round(item.percent)}%
|
|
1101
|
+
</span>
|
|
1102
|
+
</span>
|
|
1103
|
+
))}
|
|
1104
|
+
</div>
|
|
1105
|
+
</div>
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
function SegmentCompositionRow<T>({
|
|
1110
|
+
items,
|
|
1111
|
+
getKey,
|
|
1112
|
+
getValue,
|
|
1113
|
+
getColor,
|
|
1114
|
+
}: {
|
|
1115
|
+
items: T[];
|
|
1116
|
+
getKey: (item: T) => number;
|
|
1117
|
+
getValue: (item: T) => number | null | undefined;
|
|
1118
|
+
getColor: (item: T, index: number) => string;
|
|
1119
|
+
}) {
|
|
1120
|
+
const normalizedItems = buildSegmentComposition(items, getValue);
|
|
1121
|
+
|
|
1122
|
+
if (!normalizedItems.length) {
|
|
1123
|
+
return <div className="h-1.5 bg-muted/60" />;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
return (
|
|
1127
|
+
<div className="flex h-1.5 w-full overflow-hidden">
|
|
1128
|
+
{normalizedItems.map(({ item, percent }, index) => (
|
|
1129
|
+
<div
|
|
1130
|
+
key={`${getKey(item)}-${index}`}
|
|
1131
|
+
className="h-full"
|
|
1132
|
+
style={{
|
|
1133
|
+
width: `${percent}%`,
|
|
1134
|
+
backgroundColor: getColor(item, index),
|
|
1135
|
+
}}
|
|
1136
|
+
/>
|
|
1137
|
+
))}
|
|
1138
|
+
</div>
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
function SegmentTooltipContent({ segment }: { segment: SegmentView }) {
|
|
1143
|
+
return (
|
|
1144
|
+
<div className="flex flex-col gap-2 text-xs">
|
|
1145
|
+
<div className="flex items-center justify-between gap-3">
|
|
1146
|
+
<span className="font-semibold text-foreground">{segment.label}</span>
|
|
1147
|
+
<span className="font-medium text-foreground">
|
|
1148
|
+
{segment.xpValue} XP
|
|
1149
|
+
</span>
|
|
1150
|
+
</div>
|
|
1151
|
+
<div className="text-muted-foreground">
|
|
1152
|
+
{formatSeconds(segment.startSeconds)} -{' '}
|
|
1153
|
+
{formatSeconds(segment.endSeconds)}
|
|
1154
|
+
</div>
|
|
1155
|
+
<div className="grid gap-1.5">
|
|
1156
|
+
<TooltipMetric
|
|
1157
|
+
label="Área macro"
|
|
1158
|
+
value={segment.areaLabel}
|
|
1159
|
+
color={segment.areaColor}
|
|
1160
|
+
breakdown={segment.areaBreakdown}
|
|
1161
|
+
/>
|
|
1162
|
+
<TooltipMetric
|
|
1163
|
+
label="Skill"
|
|
1164
|
+
value={segment.skillLabel}
|
|
1165
|
+
color={segment.skillColor}
|
|
1166
|
+
breakdown={segment.skillBreakdown}
|
|
1167
|
+
/>
|
|
1168
|
+
<TooltipMetric
|
|
1169
|
+
label="Tipo de aprendizado"
|
|
1170
|
+
value={segment.learningTypeLabel}
|
|
1171
|
+
color={segment.learningTypeColor}
|
|
1172
|
+
breakdown={segment.learningTypeBreakdown}
|
|
1173
|
+
/>
|
|
1174
|
+
</div>
|
|
1175
|
+
</div>
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function TooltipMetric({
|
|
1180
|
+
label,
|
|
1181
|
+
value,
|
|
1182
|
+
color,
|
|
1183
|
+
breakdown,
|
|
1184
|
+
}: {
|
|
1185
|
+
label: string;
|
|
1186
|
+
value: string;
|
|
1187
|
+
color: string;
|
|
1188
|
+
breakdown: Array<{
|
|
1189
|
+
key: number;
|
|
1190
|
+
label: string;
|
|
1191
|
+
percent: number;
|
|
1192
|
+
color: string;
|
|
1193
|
+
}>;
|
|
1194
|
+
}) {
|
|
1195
|
+
return (
|
|
1196
|
+
<div className="grid gap-1">
|
|
1197
|
+
<div className="flex items-center gap-2">
|
|
1198
|
+
<span
|
|
1199
|
+
className="size-2 rounded-full"
|
|
1200
|
+
style={{ backgroundColor: color }}
|
|
1201
|
+
/>
|
|
1202
|
+
<span className="min-w-24 text-muted-foreground">{label}</span>
|
|
1203
|
+
<span className="truncate font-medium text-foreground">{value}</span>
|
|
1204
|
+
</div>
|
|
1205
|
+
{breakdown.length > 1 && (
|
|
1206
|
+
<div className="flex flex-wrap gap-1">
|
|
1207
|
+
{breakdown.map((item) => (
|
|
1208
|
+
<span
|
|
1209
|
+
key={item.key}
|
|
1210
|
+
className="inline-flex items-center gap-1 rounded-md border border-border/60 bg-muted/30 px-1.5 py-0.5 text-[10px] text-muted-foreground"
|
|
1211
|
+
>
|
|
1212
|
+
<span
|
|
1213
|
+
className="size-1.5 rounded-full"
|
|
1214
|
+
style={{ backgroundColor: item.color }}
|
|
1215
|
+
/>
|
|
1216
|
+
{item.label} {Math.round(item.percent)}%
|
|
1217
|
+
</span>
|
|
1218
|
+
))}
|
|
1219
|
+
</div>
|
|
1220
|
+
)}
|
|
1221
|
+
</div>
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
function XpStatusBadge({ status }: { status: LessonXpMapStatus }) {
|
|
1226
|
+
const config: Record<
|
|
1227
|
+
LessonXpMapStatus,
|
|
1228
|
+
{ label: string; className: string }
|
|
1229
|
+
> = {
|
|
1230
|
+
pending: { label: 'Pendente', className: 'bg-muted text-muted-foreground' },
|
|
1231
|
+
processing: {
|
|
1232
|
+
label: 'Calculando',
|
|
1233
|
+
className: 'bg-yellow-500/20 text-yellow-700 dark:text-yellow-400',
|
|
1234
|
+
},
|
|
1235
|
+
ready: {
|
|
1236
|
+
label: 'Calculado',
|
|
1237
|
+
className: 'bg-emerald-500/20 text-emerald-700 dark:text-emerald-400',
|
|
1238
|
+
},
|
|
1239
|
+
needs_review: {
|
|
1240
|
+
label: 'Revisar',
|
|
1241
|
+
className: 'bg-orange-500/20 text-orange-700 dark:text-orange-400',
|
|
1242
|
+
},
|
|
1243
|
+
approved: {
|
|
1244
|
+
label: 'Aprovado',
|
|
1245
|
+
className: 'bg-blue-500/20 text-blue-700 dark:text-blue-400',
|
|
1246
|
+
},
|
|
1247
|
+
rejected: {
|
|
1248
|
+
label: 'Rejeitado',
|
|
1249
|
+
className: 'bg-destructive/20 text-destructive',
|
|
1250
|
+
},
|
|
1251
|
+
};
|
|
1252
|
+
|
|
1253
|
+
const cfg = config[status] ?? config.pending;
|
|
1254
|
+
|
|
1255
|
+
return (
|
|
1256
|
+
<Badge
|
|
1257
|
+
variant="outline"
|
|
1258
|
+
className={cn('px-1.5 py-0 text-[0.65rem]', cfg.className)}
|
|
1259
|
+
>
|
|
1260
|
+
{cfg.label}
|
|
1261
|
+
</Badge>
|
|
1262
|
+
);
|
|
1263
|
+
}
|
|
1264
|
+
|
|
1265
|
+
function DifficultyBadge({
|
|
1266
|
+
difficulty,
|
|
1267
|
+
}: {
|
|
1268
|
+
difficulty: LessonXpSegment['difficulty'];
|
|
1269
|
+
}) {
|
|
1270
|
+
const config = {
|
|
1271
|
+
easy: 'bg-emerald-500/10 text-emerald-700 dark:text-emerald-400',
|
|
1272
|
+
medium: 'bg-blue-500/10 text-blue-700 dark:text-blue-400',
|
|
1273
|
+
hard: 'bg-orange-500/10 text-orange-700 dark:text-orange-400',
|
|
1274
|
+
expert: 'bg-rose-500/10 text-rose-700 dark:text-rose-400',
|
|
1275
|
+
};
|
|
1276
|
+
const labels = {
|
|
1277
|
+
easy: 'Fácil',
|
|
1278
|
+
medium: 'Médio',
|
|
1279
|
+
hard: 'Difícil',
|
|
1280
|
+
expert: 'Expert',
|
|
1281
|
+
};
|
|
1282
|
+
|
|
1283
|
+
return (
|
|
1284
|
+
<span
|
|
1285
|
+
className={cn('rounded px-1 py-0 text-[0.65rem]', config[difficulty])}
|
|
1286
|
+
>
|
|
1287
|
+
{labels[difficulty]}
|
|
1288
|
+
</span>
|
|
1289
|
+
);
|
|
1290
|
+
}
|
|
1291
|
+
|
|
1292
|
+
function aggregateAreas(segments: LessonXpSegment[]): Map<number, number> {
|
|
1293
|
+
const map = new Map<number, number>();
|
|
1294
|
+
for (const segment of segments) {
|
|
1295
|
+
if (!segment.shouldGrantXp) continue;
|
|
1296
|
+
for (const area of segment.areas) {
|
|
1297
|
+
const prev = map.get(area.xpAreaId) ?? 0;
|
|
1298
|
+
map.set(
|
|
1299
|
+
area.xpAreaId,
|
|
1300
|
+
prev + Math.round(segment.xpValue * (area.weightPercent / 100))
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
return new Map([...map.entries()].sort((a, b) => b[1] - a[1]));
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
function aggregateSkills(segments: LessonXpSegment[]): Map<number, number> {
|
|
1308
|
+
const map = new Map<number, number>();
|
|
1309
|
+
for (const segment of segments) {
|
|
1310
|
+
if (!segment.shouldGrantXp) continue;
|
|
1311
|
+
for (const skill of segment.skills) {
|
|
1312
|
+
const prev = map.get(skill.xpSkillId) ?? 0;
|
|
1313
|
+
map.set(
|
|
1314
|
+
skill.xpSkillId,
|
|
1315
|
+
prev + Math.round(segment.xpValue * (skill.weightPercent / 100))
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
return new Map([...map.entries()].sort((a, b) => b[1] - a[1]));
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function aggregateLearningTypes(
|
|
1323
|
+
segments: LessonXpSegment[]
|
|
1324
|
+
): Map<
|
|
1325
|
+
number,
|
|
1326
|
+
{ xp: number; count: number; learningType: LessonXpSegmentLearningType }
|
|
1327
|
+
> {
|
|
1328
|
+
const map = new Map<
|
|
1329
|
+
number,
|
|
1330
|
+
{ xp: number; count: number; learningType: LessonXpSegmentLearningType }
|
|
1331
|
+
>();
|
|
1332
|
+
for (const segment of segments) {
|
|
1333
|
+
if (!segment.shouldGrantXp) continue;
|
|
1334
|
+
for (const learningType of segment.learningTypes) {
|
|
1335
|
+
const prev = map.get(learningType.xpLearningTypeId) ?? {
|
|
1336
|
+
xp: 0,
|
|
1337
|
+
count: 0,
|
|
1338
|
+
learningType,
|
|
1339
|
+
};
|
|
1340
|
+
map.set(learningType.xpLearningTypeId, {
|
|
1341
|
+
xp:
|
|
1342
|
+
prev.xp +
|
|
1343
|
+
Math.round(segment.xpValue * (learningType.weightPercent / 100)),
|
|
1344
|
+
count: prev.count + 1,
|
|
1345
|
+
learningType: prev.learningType,
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
return new Map([...map.entries()].sort((a, b) => b[1].xp - a[1].xp));
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1352
|
+
function buildDistributionItems(
|
|
1353
|
+
aggregated: Map<number, number>,
|
|
1354
|
+
totalXp: number,
|
|
1355
|
+
resolveLabel: (id: number) => string,
|
|
1356
|
+
resolveColor: (id: number, index: number) => string
|
|
1357
|
+
): DistributionItem[] {
|
|
1358
|
+
return [...aggregated.entries()].map(([id, xp], index) => ({
|
|
1359
|
+
id,
|
|
1360
|
+
label: resolveLabel(id),
|
|
1361
|
+
xp,
|
|
1362
|
+
percent: totalXp > 0 ? Math.round((xp / totalXp) * 100) : 0,
|
|
1363
|
+
color: resolveColor(id, index),
|
|
1364
|
+
}));
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
function pickDominantItem<T extends { weightPercent: number }>(
|
|
1368
|
+
items: T[]
|
|
1369
|
+
): T | null {
|
|
1370
|
+
if (!items.length) return null;
|
|
1371
|
+
return (
|
|
1372
|
+
[...items].sort((a, b) => b.weightPercent - a.weightPercent)[0] ?? null
|
|
1373
|
+
);
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
function buildSegmentComposition<T>(
|
|
1377
|
+
items: T[],
|
|
1378
|
+
getValue: (item: T) => number | null | undefined
|
|
1379
|
+
) {
|
|
1380
|
+
if (!items.length) return [];
|
|
1381
|
+
|
|
1382
|
+
const resolved = items.map((item) => ({
|
|
1383
|
+
item,
|
|
1384
|
+
value: Math.max(getValue(item) ?? 0, 0),
|
|
1385
|
+
}));
|
|
1386
|
+
const total = resolved.reduce((sum, entry) => sum + entry.value, 0);
|
|
1387
|
+
|
|
1388
|
+
if (total > 0) {
|
|
1389
|
+
return resolved.map((entry) => ({
|
|
1390
|
+
item: entry.item,
|
|
1391
|
+
percent: (entry.value / total) * 100,
|
|
1392
|
+
}));
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
const fallbackPercent = 100 / resolved.length;
|
|
1396
|
+
return resolved.map((entry) => ({
|
|
1397
|
+
item: entry.item,
|
|
1398
|
+
percent: fallbackPercent,
|
|
1399
|
+
}));
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
function countUniqueAreas(segments: LessonXpSegment[]): number {
|
|
1403
|
+
const ids = new Set<number>();
|
|
1404
|
+
for (const segment of segments)
|
|
1405
|
+
segment.areas.forEach((area) => ids.add(area.xpAreaId));
|
|
1406
|
+
return ids.size;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
function countUniqueSkills(segments: LessonXpSegment[]): number {
|
|
1410
|
+
const ids = new Set<number>();
|
|
1411
|
+
for (const segment of segments)
|
|
1412
|
+
segment.skills.forEach((skill) => ids.add(skill.xpSkillId));
|
|
1413
|
+
return ids.size;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
function countUniqueLearningTypes(segments: LessonXpSegment[]): number {
|
|
1417
|
+
const ids = new Set<number>();
|
|
1418
|
+
for (const segment of segments) {
|
|
1419
|
+
segment.learningTypes.forEach((learningType) =>
|
|
1420
|
+
ids.add(learningType.xpLearningTypeId)
|
|
1421
|
+
);
|
|
1422
|
+
}
|
|
1423
|
+
return ids.size;
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
function avgConfidence(segments: LessonXpSegment[]): number | null {
|
|
1427
|
+
const values = segments
|
|
1428
|
+
.map((segment) => segment.aiConfidence)
|
|
1429
|
+
.filter((value): value is number => value !== null);
|
|
1430
|
+
if (!values.length) return null;
|
|
1431
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
function formatSeconds(seconds: number): string {
|
|
1435
|
+
const minutes = Math.floor(seconds / 60);
|
|
1436
|
+
const restSeconds = Math.floor(seconds % 60);
|
|
1437
|
+
return `${String(minutes).padStart(2, '0')}:${String(restSeconds).padStart(2, '0')}`;
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
function formatDate(iso: string): string {
|
|
1441
|
+
try {
|
|
1442
|
+
return new Date(iso).toLocaleDateString('pt-BR', {
|
|
1443
|
+
day: '2-digit',
|
|
1444
|
+
month: '2-digit',
|
|
1445
|
+
year: 'numeric',
|
|
1446
|
+
});
|
|
1447
|
+
} catch {
|
|
1448
|
+
return iso;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
function formatMultiplier(value: number, locale: string): string {
|
|
1453
|
+
const fractionDigits = Number.isInteger(value) ? 0 : 2;
|
|
1454
|
+
return `x${new Intl.NumberFormat(locale, {
|
|
1455
|
+
minimumFractionDigits: fractionDigits,
|
|
1456
|
+
maximumFractionDigits: 2,
|
|
1457
|
+
}).format(value)}`;
|
|
1458
|
+
}
|