@iblai/iblai-js 1.3.12 → 1.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
2
|
import React__default, { useRef, useEffect, useState, useCallback, useLayoutEffect, forwardRef, createElement as createElement$1, useMemo, createContext, useReducer, useImperativeHandle, useContext, useDebugValue, isValidElement, Children, PureComponent, cloneElement, Component } from 'react';
|
|
3
3
|
import { LOCAL_STORAGE_KEYS, TimeTracker, getInitials, useTenantMetadata, isAlphaNumeric32, checkRbacPermission, getTimeAgo, formatRelativeTime as formatRelativeTime$2, ANONYMOUS_USERNAME, combineCSVData, redirectToAuthSpaJoinTenant, redirectToAuthSpa, getAuthSpaJoinUrl } from '@iblai/web-utils';
|
|
4
|
-
import { useTimeTrackingMutation, useGetUserMetadataQuery, useGetUserMetadataEdxQuery, useUpdateUserMetadataMutation, useUpdateUserMetadataEdxMutation, useUploadProfileImageMutation, useResetPasswordMutation, useSelfRetireMutation, useCreateUserInstitutionMutation, useGetUserInstitutionsQuery, useCreateUserEducationMutation, useUpdateUserEducationMutation, useDeleteUserEducationMutation, useGetUserEducationQuery, useCreateUserCompanyMutation, useGetUserCompaniesQuery, useCreateUserExperienceMutation, useUpdateUserExperienceMutation, useDeleteUserExperienceMutation, useGetUserExperienceQuery, useGetUserResumeQuery, useCreateUserResumeMutation, useGetMySubscriptionsQuery, useGetItemSubscriptionQuery, useCancelSubscriptionMutation, useCreateGlobalMemoryMutation,
|
|
4
|
+
import { useTimeTrackingMutation, useGetUserMetadataQuery, useGetUserMetadataEdxQuery, useUpdateUserMetadataMutation, useUpdateUserMetadataEdxMutation, useUploadProfileImageMutation, useResetPasswordMutation, useSelfRetireMutation, useCreateUserInstitutionMutation, useGetUserInstitutionsQuery, useCreateUserEducationMutation, useUpdateUserEducationMutation, useDeleteUserEducationMutation, useGetUserEducationQuery, useCreateUserCompanyMutation, useGetUserCompaniesQuery, useCreateUserExperienceMutation, useUpdateUserExperienceMutation, useDeleteUserExperienceMutation, useGetUserExperienceQuery, useGetUserResumeQuery, useCreateUserResumeMutation, useGetMySubscriptionsQuery, useGetItemSubscriptionQuery, useCancelSubscriptionMutation, useCreateGlobalMemoryMutation, useGetMemsearchStatusQuery, useGetUserMemorySettingsQuery, useUpdateUserMemorySettingsMutation, useGetGlobalMemoriesQuery, useDeleteGlobalMemoryMutation, useInviteUserMutation, usePlatformInvitationsQuery, useCreateCatalogInvitationCourseBulkMutation, useGetCatalogInvitationsCourseQuery, useLazyPlatformUsersQuery, useLazyPlatformUserGroupsQuery, useGetPersonnalizedSearchQuery, useCreateCatalogInvitationProgramBulkMutation, useGetCatalogInvitationsProgramQuery, useGetNotificationsCountQuery, useLazyGetNotificationsQuery, useMarkAllAsReadMutation, useCreateNotificationPreviewMutation, useSendNotificationMutation, useGetMentorsQuery, useGetRbacPoliciesQuery, usePlatformUsersQuery, usePlatformUserGroupsQuery, useUpdateTemplateMutation, useGetTemplateDetailsQuery, useGetTemplatesQuery, useLazyGetTemplateDetailsQuery, useToggleTemplateMutation, useGetTopicsStatsQuery, useGetUsersStatsQuery, useGetSessionStatsQuery, useGetTopicsDetailsStatsQuery, useGetAccessTimeHeatmapQuery, useGetUserDetailsStatsQuery, useGetTranscriptsConversationHeadlineQuery, useGetAverageRatingQuery, useGetFinancialStatsQuery, useGetDetailedFinancialStatsQuery, useGetTranscriptsMessagesDetailsQuery, useGetTranscriptsMessagesQuery, useGetReportDetailQuery, useLazyGetDownloadReportFromURLQuery, useGetReportsQuery, useCreateReportMutation, useGetMentorPublicSettingsQuery, useGetContentAnalyticsQuery, useGetContentAnalyticsDetailsQuery, useGetRevenueQuery, useListPaywallsQuery, useListSubscribersQuery, useLazyGetCourseMetaDataQuery, useLazyGetCourseCompletionOutlinesQuery, useLazyGetCourseEligibilityQuery, useLazyGetUserEnrolledCoursesQuery, useLazyGetUserAssignedCoursesQuery, useLazyGetUserCredentialsQuery, useLazyGetOverTimeActivityQuery, useLazyGetCatalogSearchQuery, useGetUserEarnedSkillsQuery, useGetUserReportedSkillsQuery, useGetUserDesiredSkillsQuery, useCreateOrUpdateUserReportedSkillMutation, useCreateOrUpdateUserDesiredSkillMutation, useLazyGetPathwayListQuery, useLazyGetUserAssignedPathwaysQuery, useLazyGetUserEnrolledPathwaysQuery, useLazyGetPathwayCompletionQuery, useLazyGetProgramListQuery, useLazyGetProgramCompletionQuery, useLazyGetUserEnrolledProgramsQuery, useLazyGetAssignedProgramsQuery, useLazyGetUserSkillsPointsQuery, useLazyGetUserReportedSkillsQuery, useLazyGetUserDesiredSkillsQuery, useLazyGetUserCatalogPathwaysQuery, useLazyGetPerLearnerInfoQuery, useLazyGetEdxSSOTokenQuery, useCreateCourseEnrollmentMutation, useCreateStripeCheckoutSessionMutation, useLazyGetCourseProgressQuery, useLazyGetCourseCompletionQuery, useUpdateExamAttemptMutation, useStartExamMutation, useLazyGetExamInfoQuery, useCreateCheckoutMutation } from '@iblai/data-layer';
|
|
5
5
|
import { toast, Toaster as Toaster$1 } from 'sonner';
|
|
6
6
|
import { jsx, Fragment as Fragment$1, jsxs } from 'react/jsx-runtime';
|
|
7
7
|
import * as ReactDOM from 'react-dom';
|
|
@@ -10,6 +10,7 @@ import { z as z$1 } from 'zod';
|
|
|
10
10
|
import { NotificationSourceTypeEnum, StateEnum } from '@iblai/iblai-api';
|
|
11
11
|
import { useSearchParams, useRouter, usePathname } from 'next/navigation';
|
|
12
12
|
import { skipToken } from '@reduxjs/toolkit/query';
|
|
13
|
+
import Image$4 from 'next/image';
|
|
13
14
|
|
|
14
15
|
function useIframeMessageHandler({ handlers, allowedOrigins, defaultHandler, }) {
|
|
15
16
|
const handlersRef = useRef(handlers);
|
|
@@ -49141,12 +49142,12 @@ const createLucideIcon = (iconName, iconNode) => {
|
|
|
49141
49142
|
*/
|
|
49142
49143
|
|
|
49143
49144
|
|
|
49144
|
-
const __iconNode$
|
|
49145
|
+
const __iconNode$1p = [
|
|
49145
49146
|
["path", { d: "M17 12H7", key: "16if0g" }],
|
|
49146
49147
|
["path", { d: "M19 18H5", key: "18s9l3" }],
|
|
49147
49148
|
["path", { d: "M21 6H3", key: "1jwq7v" }]
|
|
49148
49149
|
];
|
|
49149
|
-
const AlignCenter = createLucideIcon("align-center", __iconNode$
|
|
49150
|
+
const AlignCenter = createLucideIcon("align-center", __iconNode$1p);
|
|
49150
49151
|
|
|
49151
49152
|
/**
|
|
49152
49153
|
* @license lucide-react v0.507.0 - ISC
|
|
@@ -49156,12 +49157,12 @@ const AlignCenter = createLucideIcon("align-center", __iconNode$1o);
|
|
|
49156
49157
|
*/
|
|
49157
49158
|
|
|
49158
49159
|
|
|
49159
|
-
const __iconNode$
|
|
49160
|
+
const __iconNode$1o = [
|
|
49160
49161
|
["path", { d: "M15 12H3", key: "6jk70r" }],
|
|
49161
49162
|
["path", { d: "M17 18H3", key: "1amg6g" }],
|
|
49162
49163
|
["path", { d: "M21 6H3", key: "1jwq7v" }]
|
|
49163
49164
|
];
|
|
49164
|
-
const AlignLeft = createLucideIcon("align-left", __iconNode$
|
|
49165
|
+
const AlignLeft = createLucideIcon("align-left", __iconNode$1o);
|
|
49165
49166
|
|
|
49166
49167
|
/**
|
|
49167
49168
|
* @license lucide-react v0.507.0 - ISC
|
|
@@ -49171,12 +49172,12 @@ const AlignLeft = createLucideIcon("align-left", __iconNode$1n);
|
|
|
49171
49172
|
*/
|
|
49172
49173
|
|
|
49173
49174
|
|
|
49174
|
-
const __iconNode$
|
|
49175
|
+
const __iconNode$1n = [
|
|
49175
49176
|
["path", { d: "M21 12H9", key: "dn1m92" }],
|
|
49176
49177
|
["path", { d: "M21 18H7", key: "1ygte8" }],
|
|
49177
49178
|
["path", { d: "M21 6H3", key: "1jwq7v" }]
|
|
49178
49179
|
];
|
|
49179
|
-
const AlignRight = createLucideIcon("align-right", __iconNode$
|
|
49180
|
+
const AlignRight = createLucideIcon("align-right", __iconNode$1n);
|
|
49180
49181
|
|
|
49181
49182
|
/**
|
|
49182
49183
|
* @license lucide-react v0.507.0 - ISC
|
|
@@ -49186,12 +49187,12 @@ const AlignRight = createLucideIcon("align-right", __iconNode$1m);
|
|
|
49186
49187
|
*/
|
|
49187
49188
|
|
|
49188
49189
|
|
|
49189
|
-
const __iconNode$
|
|
49190
|
+
const __iconNode$1m = [
|
|
49190
49191
|
["rect", { width: "20", height: "5", x: "2", y: "3", rx: "1", key: "1wp1u1" }],
|
|
49191
49192
|
["path", { d: "M4 8v11a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8", key: "1s80jp" }],
|
|
49192
49193
|
["path", { d: "M10 12h4", key: "a56b0p" }]
|
|
49193
49194
|
];
|
|
49194
|
-
const Archive = createLucideIcon("archive", __iconNode$
|
|
49195
|
+
const Archive = createLucideIcon("archive", __iconNode$1m);
|
|
49195
49196
|
|
|
49196
49197
|
/**
|
|
49197
49198
|
* @license lucide-react v0.507.0 - ISC
|
|
@@ -49201,11 +49202,11 @@ const Archive = createLucideIcon("archive", __iconNode$1l);
|
|
|
49201
49202
|
*/
|
|
49202
49203
|
|
|
49203
49204
|
|
|
49204
|
-
const __iconNode$
|
|
49205
|
+
const __iconNode$1l = [
|
|
49205
49206
|
["path", { d: "m12 19-7-7 7-7", key: "1l729n" }],
|
|
49206
49207
|
["path", { d: "M19 12H5", key: "x3x0zl" }]
|
|
49207
49208
|
];
|
|
49208
|
-
const ArrowLeft = createLucideIcon("arrow-left", __iconNode$
|
|
49209
|
+
const ArrowLeft = createLucideIcon("arrow-left", __iconNode$1l);
|
|
49209
49210
|
|
|
49210
49211
|
/**
|
|
49211
49212
|
* @license lucide-react v0.507.0 - ISC
|
|
@@ -49215,7 +49216,7 @@ const ArrowLeft = createLucideIcon("arrow-left", __iconNode$1k);
|
|
|
49215
49216
|
*/
|
|
49216
49217
|
|
|
49217
49218
|
|
|
49218
|
-
const __iconNode$
|
|
49219
|
+
const __iconNode$1k = [
|
|
49219
49220
|
["path", { d: "M10.268 21a2 2 0 0 0 3.464 0", key: "vwvbt9" }],
|
|
49220
49221
|
[
|
|
49221
49222
|
"path",
|
|
@@ -49225,7 +49226,7 @@ const __iconNode$1j = [
|
|
|
49225
49226
|
}
|
|
49226
49227
|
]
|
|
49227
49228
|
];
|
|
49228
|
-
const Bell = createLucideIcon("bell", __iconNode$
|
|
49229
|
+
const Bell = createLucideIcon("bell", __iconNode$1k);
|
|
49229
49230
|
|
|
49230
49231
|
/**
|
|
49231
49232
|
* @license lucide-react v0.507.0 - ISC
|
|
@@ -49235,13 +49236,13 @@ const Bell = createLucideIcon("bell", __iconNode$1j);
|
|
|
49235
49236
|
*/
|
|
49236
49237
|
|
|
49237
49238
|
|
|
49238
|
-
const __iconNode$
|
|
49239
|
+
const __iconNode$1j = [
|
|
49239
49240
|
[
|
|
49240
49241
|
"path",
|
|
49241
49242
|
{ d: "M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8", key: "mg9rjx" }
|
|
49242
49243
|
]
|
|
49243
49244
|
];
|
|
49244
|
-
const Bold$1 = createLucideIcon("bold", __iconNode$
|
|
49245
|
+
const Bold$1 = createLucideIcon("bold", __iconNode$1j);
|
|
49245
49246
|
|
|
49246
49247
|
/**
|
|
49247
49248
|
* @license lucide-react v0.507.0 - ISC
|
|
@@ -49251,7 +49252,7 @@ const Bold$1 = createLucideIcon("bold", __iconNode$1i);
|
|
|
49251
49252
|
*/
|
|
49252
49253
|
|
|
49253
49254
|
|
|
49254
|
-
const __iconNode$
|
|
49255
|
+
const __iconNode$1i = [
|
|
49255
49256
|
["path", { d: "M12 7v14", key: "1akyts" }],
|
|
49256
49257
|
[
|
|
49257
49258
|
"path",
|
|
@@ -49261,7 +49262,7 @@ const __iconNode$1h = [
|
|
|
49261
49262
|
}
|
|
49262
49263
|
]
|
|
49263
49264
|
];
|
|
49264
|
-
const BookOpen = createLucideIcon("book-open", __iconNode$
|
|
49265
|
+
const BookOpen = createLucideIcon("book-open", __iconNode$1i);
|
|
49265
49266
|
|
|
49266
49267
|
/**
|
|
49267
49268
|
* @license lucide-react v0.507.0 - ISC
|
|
@@ -49271,7 +49272,7 @@ const BookOpen = createLucideIcon("book-open", __iconNode$1h);
|
|
|
49271
49272
|
*/
|
|
49272
49273
|
|
|
49273
49274
|
|
|
49274
|
-
const __iconNode$
|
|
49275
|
+
const __iconNode$1h = [
|
|
49275
49276
|
["path", { d: "M12 8V4H8", key: "hb8ula" }],
|
|
49276
49277
|
["rect", { width: "16", height: "12", x: "4", y: "8", rx: "2", key: "enze0r" }],
|
|
49277
49278
|
["path", { d: "M2 14h2", key: "vft8re" }],
|
|
@@ -49279,7 +49280,7 @@ const __iconNode$1g = [
|
|
|
49279
49280
|
["path", { d: "M15 13v2", key: "1xurst" }],
|
|
49280
49281
|
["path", { d: "M9 13v2", key: "rq6x2g" }]
|
|
49281
49282
|
];
|
|
49282
|
-
const Bot = createLucideIcon("bot", __iconNode$
|
|
49283
|
+
const Bot = createLucideIcon("bot", __iconNode$1h);
|
|
49283
49284
|
|
|
49284
49285
|
/**
|
|
49285
49286
|
* @license lucide-react v0.507.0 - ISC
|
|
@@ -49289,11 +49290,11 @@ const Bot = createLucideIcon("bot", __iconNode$1g);
|
|
|
49289
49290
|
*/
|
|
49290
49291
|
|
|
49291
49292
|
|
|
49292
|
-
const __iconNode$
|
|
49293
|
+
const __iconNode$1g = [
|
|
49293
49294
|
["path", { d: "M16 20V4a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16", key: "jecpp" }],
|
|
49294
49295
|
["rect", { width: "20", height: "14", x: "2", y: "6", rx: "2", key: "i6l2r4" }]
|
|
49295
49296
|
];
|
|
49296
|
-
const Briefcase = createLucideIcon("briefcase", __iconNode$
|
|
49297
|
+
const Briefcase = createLucideIcon("briefcase", __iconNode$1g);
|
|
49297
49298
|
|
|
49298
49299
|
/**
|
|
49299
49300
|
* @license lucide-react v0.507.0 - ISC
|
|
@@ -49303,13 +49304,13 @@ const Briefcase = createLucideIcon("briefcase", __iconNode$1f);
|
|
|
49303
49304
|
*/
|
|
49304
49305
|
|
|
49305
49306
|
|
|
49306
|
-
const __iconNode$
|
|
49307
|
+
const __iconNode$1f = [
|
|
49307
49308
|
["path", { d: "M8 2v4", key: "1cmpym" }],
|
|
49308
49309
|
["path", { d: "M16 2v4", key: "4m81vk" }],
|
|
49309
49310
|
["rect", { width: "18", height: "18", x: "3", y: "4", rx: "2", key: "1hopcy" }],
|
|
49310
49311
|
["path", { d: "M3 10h18", key: "8toen8" }]
|
|
49311
49312
|
];
|
|
49312
|
-
const Calendar$1 = createLucideIcon("calendar", __iconNode$
|
|
49313
|
+
const Calendar$1 = createLucideIcon("calendar", __iconNode$1f);
|
|
49313
49314
|
|
|
49314
49315
|
/**
|
|
49315
49316
|
* @license lucide-react v0.507.0 - ISC
|
|
@@ -49319,11 +49320,11 @@ const Calendar$1 = createLucideIcon("calendar", __iconNode$1e);
|
|
|
49319
49320
|
*/
|
|
49320
49321
|
|
|
49321
49322
|
|
|
49322
|
-
const __iconNode$
|
|
49323
|
+
const __iconNode$1e = [
|
|
49323
49324
|
["path", { d: "M18 6 7 17l-5-5", key: "116fxf" }],
|
|
49324
49325
|
["path", { d: "m22 10-7.5 7.5L13 16", key: "ke71qq" }]
|
|
49325
49326
|
];
|
|
49326
|
-
const CheckCheck = createLucideIcon("check-check", __iconNode$
|
|
49327
|
+
const CheckCheck = createLucideIcon("check-check", __iconNode$1e);
|
|
49327
49328
|
|
|
49328
49329
|
/**
|
|
49329
49330
|
* @license lucide-react v0.507.0 - ISC
|
|
@@ -49333,8 +49334,8 @@ const CheckCheck = createLucideIcon("check-check", __iconNode$1d);
|
|
|
49333
49334
|
*/
|
|
49334
49335
|
|
|
49335
49336
|
|
|
49336
|
-
const __iconNode$
|
|
49337
|
-
const Check = createLucideIcon("check", __iconNode$
|
|
49337
|
+
const __iconNode$1d = [["path", { d: "M20 6 9 17l-5-5", key: "1gmf2c" }]];
|
|
49338
|
+
const Check = createLucideIcon("check", __iconNode$1d);
|
|
49338
49339
|
|
|
49339
49340
|
/**
|
|
49340
49341
|
* @license lucide-react v0.507.0 - ISC
|
|
@@ -49344,8 +49345,8 @@ const Check = createLucideIcon("check", __iconNode$1c);
|
|
|
49344
49345
|
*/
|
|
49345
49346
|
|
|
49346
49347
|
|
|
49347
|
-
const __iconNode$
|
|
49348
|
-
const ChevronDown = createLucideIcon("chevron-down", __iconNode$
|
|
49348
|
+
const __iconNode$1c = [["path", { d: "m6 9 6 6 6-6", key: "qrunsl" }]];
|
|
49349
|
+
const ChevronDown = createLucideIcon("chevron-down", __iconNode$1c);
|
|
49349
49350
|
|
|
49350
49351
|
/**
|
|
49351
49352
|
* @license lucide-react v0.507.0 - ISC
|
|
@@ -49355,11 +49356,11 @@ const ChevronDown = createLucideIcon("chevron-down", __iconNode$1b);
|
|
|
49355
49356
|
*/
|
|
49356
49357
|
|
|
49357
49358
|
|
|
49358
|
-
const __iconNode$
|
|
49359
|
+
const __iconNode$1b = [
|
|
49359
49360
|
["path", { d: "m17 18-6-6 6-6", key: "1yerx2" }],
|
|
49360
49361
|
["path", { d: "M7 6v12", key: "1p53r6" }]
|
|
49361
49362
|
];
|
|
49362
|
-
const ChevronFirst = createLucideIcon("chevron-first", __iconNode$
|
|
49363
|
+
const ChevronFirst = createLucideIcon("chevron-first", __iconNode$1b);
|
|
49363
49364
|
|
|
49364
49365
|
/**
|
|
49365
49366
|
* @license lucide-react v0.507.0 - ISC
|
|
@@ -49369,8 +49370,8 @@ const ChevronFirst = createLucideIcon("chevron-first", __iconNode$1a);
|
|
|
49369
49370
|
*/
|
|
49370
49371
|
|
|
49371
49372
|
|
|
49372
|
-
const __iconNode$
|
|
49373
|
-
const ChevronLeft = createLucideIcon("chevron-left", __iconNode$
|
|
49373
|
+
const __iconNode$1a = [["path", { d: "m15 18-6-6 6-6", key: "1wnfg3" }]];
|
|
49374
|
+
const ChevronLeft = createLucideIcon("chevron-left", __iconNode$1a);
|
|
49374
49375
|
|
|
49375
49376
|
/**
|
|
49376
49377
|
* @license lucide-react v0.507.0 - ISC
|
|
@@ -49380,11 +49381,11 @@ const ChevronLeft = createLucideIcon("chevron-left", __iconNode$19);
|
|
|
49380
49381
|
*/
|
|
49381
49382
|
|
|
49382
49383
|
|
|
49383
|
-
const __iconNode$
|
|
49384
|
+
const __iconNode$19 = [
|
|
49384
49385
|
["path", { d: "m7 18 6-6-6-6", key: "lwmzdw" }],
|
|
49385
49386
|
["path", { d: "M17 6v12", key: "1o0aio" }]
|
|
49386
49387
|
];
|
|
49387
|
-
const ChevronLast = createLucideIcon("chevron-last", __iconNode$
|
|
49388
|
+
const ChevronLast = createLucideIcon("chevron-last", __iconNode$19);
|
|
49388
49389
|
|
|
49389
49390
|
/**
|
|
49390
49391
|
* @license lucide-react v0.507.0 - ISC
|
|
@@ -49394,8 +49395,8 @@ const ChevronLast = createLucideIcon("chevron-last", __iconNode$18);
|
|
|
49394
49395
|
*/
|
|
49395
49396
|
|
|
49396
49397
|
|
|
49397
|
-
const __iconNode$
|
|
49398
|
-
const ChevronRight = createLucideIcon("chevron-right", __iconNode$
|
|
49398
|
+
const __iconNode$18 = [["path", { d: "m9 18 6-6-6-6", key: "mthhwq" }]];
|
|
49399
|
+
const ChevronRight = createLucideIcon("chevron-right", __iconNode$18);
|
|
49399
49400
|
|
|
49400
49401
|
/**
|
|
49401
49402
|
* @license lucide-react v0.507.0 - ISC
|
|
@@ -49405,8 +49406,8 @@ const ChevronRight = createLucideIcon("chevron-right", __iconNode$17);
|
|
|
49405
49406
|
*/
|
|
49406
49407
|
|
|
49407
49408
|
|
|
49408
|
-
const __iconNode$
|
|
49409
|
-
const ChevronUp = createLucideIcon("chevron-up", __iconNode$
|
|
49409
|
+
const __iconNode$17 = [["path", { d: "m18 15-6-6-6 6", key: "153udz" }]];
|
|
49410
|
+
const ChevronUp = createLucideIcon("chevron-up", __iconNode$17);
|
|
49410
49411
|
|
|
49411
49412
|
/**
|
|
49412
49413
|
* @license lucide-react v0.507.0 - ISC
|
|
@@ -49416,11 +49417,11 @@ const ChevronUp = createLucideIcon("chevron-up", __iconNode$16);
|
|
|
49416
49417
|
*/
|
|
49417
49418
|
|
|
49418
49419
|
|
|
49419
|
-
const __iconNode$
|
|
49420
|
+
const __iconNode$16 = [
|
|
49420
49421
|
["path", { d: "m11 17-5-5 5-5", key: "13zhaf" }],
|
|
49421
49422
|
["path", { d: "m18 17-5-5 5-5", key: "h8a8et" }]
|
|
49422
49423
|
];
|
|
49423
|
-
const ChevronsLeft = createLucideIcon("chevrons-left", __iconNode$
|
|
49424
|
+
const ChevronsLeft = createLucideIcon("chevrons-left", __iconNode$16);
|
|
49424
49425
|
|
|
49425
49426
|
/**
|
|
49426
49427
|
* @license lucide-react v0.507.0 - ISC
|
|
@@ -49430,11 +49431,26 @@ const ChevronsLeft = createLucideIcon("chevrons-left", __iconNode$15);
|
|
|
49430
49431
|
*/
|
|
49431
49432
|
|
|
49432
49433
|
|
|
49433
|
-
const __iconNode$
|
|
49434
|
+
const __iconNode$15 = [
|
|
49434
49435
|
["path", { d: "m6 17 5-5-5-5", key: "xnjwq" }],
|
|
49435
49436
|
["path", { d: "m13 17 5-5-5-5", key: "17xmmf" }]
|
|
49436
49437
|
];
|
|
49437
|
-
const ChevronsRight = createLucideIcon("chevrons-right", __iconNode$
|
|
49438
|
+
const ChevronsRight = createLucideIcon("chevrons-right", __iconNode$15);
|
|
49439
|
+
|
|
49440
|
+
/**
|
|
49441
|
+
* @license lucide-react v0.507.0 - ISC
|
|
49442
|
+
*
|
|
49443
|
+
* This source code is licensed under the ISC license.
|
|
49444
|
+
* See the LICENSE file in the root directory of this source tree.
|
|
49445
|
+
*/
|
|
49446
|
+
|
|
49447
|
+
|
|
49448
|
+
const __iconNode$14 = [
|
|
49449
|
+
["circle", { cx: "12", cy: "12", r: "10", key: "1mglay" }],
|
|
49450
|
+
["line", { x1: "12", x2: "12", y1: "8", y2: "12", key: "1pkeuh" }],
|
|
49451
|
+
["line", { x1: "12", x2: "12.01", y1: "16", y2: "16", key: "4dfq90" }]
|
|
49452
|
+
];
|
|
49453
|
+
const CircleAlert = createLucideIcon("circle-alert", __iconNode$14);
|
|
49438
49454
|
|
|
49439
49455
|
/**
|
|
49440
49456
|
* @license lucide-react v0.507.0 - ISC
|
|
@@ -49585,17 +49601,11 @@ const Database = createLucideIcon("database", __iconNode$W);
|
|
|
49585
49601
|
|
|
49586
49602
|
|
|
49587
49603
|
const __iconNode$V = [
|
|
49588
|
-
[
|
|
49589
|
-
|
|
49590
|
-
|
|
49591
|
-
d: "M10 5a2 2 0 0 0-1.344.519l-6.328 5.74a1 1 0 0 0 0 1.481l6.328 5.741A2 2 0 0 0 10 19h10a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2z",
|
|
49592
|
-
key: "1yo7s0"
|
|
49593
|
-
}
|
|
49594
|
-
],
|
|
49595
|
-
["path", { d: "m12 9 6 6", key: "anjzzh" }],
|
|
49596
|
-
["path", { d: "m18 9-6 6", key: "1fp51s" }]
|
|
49604
|
+
["path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4", key: "ih7n3h" }],
|
|
49605
|
+
["polyline", { points: "7 10 12 15 17 10", key: "2ggqvy" }],
|
|
49606
|
+
["line", { x1: "12", x2: "12", y1: "15", y2: "3", key: "1vk2je" }]
|
|
49597
49607
|
];
|
|
49598
|
-
const
|
|
49608
|
+
const Download = createLucideIcon("download", __iconNode$V);
|
|
49599
49609
|
|
|
49600
49610
|
/**
|
|
49601
49611
|
* @license lucide-react v0.507.0 - ISC
|
|
@@ -49606,11 +49616,11 @@ const Delete$1 = createLucideIcon("delete", __iconNode$V);
|
|
|
49606
49616
|
|
|
49607
49617
|
|
|
49608
49618
|
const __iconNode$U = [
|
|
49609
|
-
["
|
|
49610
|
-
["
|
|
49611
|
-
["
|
|
49619
|
+
["circle", { cx: "12", cy: "12", r: "1", key: "41hilf" }],
|
|
49620
|
+
["circle", { cx: "19", cy: "12", r: "1", key: "1wjl8i" }],
|
|
49621
|
+
["circle", { cx: "5", cy: "12", r: "1", key: "1pcz8c" }]
|
|
49612
49622
|
];
|
|
49613
|
-
const
|
|
49623
|
+
const Ellipsis = createLucideIcon("ellipsis", __iconNode$U);
|
|
49614
49624
|
|
|
49615
49625
|
/**
|
|
49616
49626
|
* @license lucide-react v0.507.0 - ISC
|
|
@@ -82016,12 +82026,82 @@ function AddMemoryDialog({ open, onOpenChange, org, username }) {
|
|
|
82016
82026
|
}, children: jsx(DialogContent$1, { className: "p-0 overflow-hidden max-w-lg", children: jsxs("form", { onSubmit: handleSubmit, children: [jsx("div", { className: "flex items-center justify-between px-6 py-4 border-b", children: jsx(DialogTitle$1, { className: "text-lg font-semibold text-gray-900", children: "Add Memory" }) }), jsx(DialogDescription$1, { className: "sr-only", children: "Add a new memory for the AI to remember about you" }), jsx("div", { className: "p-6 space-y-4", children: jsxs("div", { className: "space-y-2", children: [jsx(Label$2, { htmlFor: "memory-content", children: "What should the AI remember?" }), jsx(Textarea, { id: "memory-content", placeholder: "e.g., I prefer learning through practical examples rather than theory.", value: content, onChange: (e) => setContent(e.target.value), rows: 4, className: "w-full bg-gray-50 border-gray-200 focus:ring-blue-500 focus:border-blue-500 text-sm resize-none", disabled: isLoading }), content.length > 0 && content.trim().length < 10 && (jsx("p", { className: "text-sm text-red-500", children: "Must be at least 10 characters" }))] }) }), jsxs("div", { className: "flex justify-end gap-3 border-t px-6 py-4", children: [jsx(Button$1, { type: "button", variant: "outline", onClick: () => onOpenChange(false), disabled: isLoading, children: "Cancel" }), jsx(Button$1, { className: "bg-gradient-to-r from-[#2563EB] to-[#93C5FD] hover:opacity-90 text-white", type: "submit", disabled: isLoading || content.trim().length < 10, children: isLoading ? 'Saving...' : 'Save Memory' })] })] }) }) }));
|
|
82017
82027
|
}
|
|
82018
82028
|
|
|
82029
|
+
const Pagination = ({ className, ...props }) => (jsx("nav", { role: "navigation", "aria-label": "pagination", className: cn("mx-auto flex w-full justify-center", className), ...props }));
|
|
82030
|
+
Pagination.displayName = "Pagination";
|
|
82031
|
+
const PaginationContent = React.forwardRef(({ className, ...props }, ref) => (jsx("ul", { ref: ref, className: cn("flex flex-row items-center gap-1", className), ...props })));
|
|
82032
|
+
PaginationContent.displayName = "PaginationContent";
|
|
82033
|
+
const PaginationItem = React.forwardRef(({ className, ...props }, ref) => (jsx("li", { ref: ref, className: cn("", className), ...props })));
|
|
82034
|
+
PaginationItem.displayName = "PaginationItem";
|
|
82035
|
+
const PaginationLink = ({ className, isActive, size = "icon", ...props }) => (jsx("a", { "aria-current": isActive ? "page" : undefined, className: cn(buttonVariants({
|
|
82036
|
+
variant: isActive ? "outline" : "ghost",
|
|
82037
|
+
size,
|
|
82038
|
+
}), className), ...props }));
|
|
82039
|
+
PaginationLink.displayName = "PaginationLink";
|
|
82040
|
+
const PaginationPrevious = ({ className, ...props }) => (jsxs(PaginationLink, { "aria-label": "Go to previous page", size: "default", className: cn("gap-1 pl-2.5", className), ...props, children: [jsx(ChevronLeft, { className: "h-4 w-4" }), jsx("span", { children: "Previous" })] }));
|
|
82041
|
+
PaginationPrevious.displayName = "PaginationPrevious";
|
|
82042
|
+
const PaginationNext = ({ className, ...props }) => (jsxs(PaginationLink, { "aria-label": "Go to next page", size: "default", className: cn("gap-1 pr-2.5", className), ...props, children: [jsx("span", { children: "Next" }), jsx(ChevronRight, { className: "h-4 w-4" })] }));
|
|
82043
|
+
PaginationNext.displayName = "PaginationNext";
|
|
82044
|
+
const PaginationEllipsis = ({ className, ...props }) => (jsxs("span", { "aria-hidden": true, className: cn("flex h-9 w-9 items-center justify-center", className), ...props, children: [jsx(Ellipsis, { className: "h-4 w-4" }), jsx("span", { className: "sr-only", children: "More pages" })] }));
|
|
82045
|
+
PaginationEllipsis.displayName = "PaginationEllipsis";
|
|
82046
|
+
|
|
82047
|
+
const IblPagination = ({ currentPage, totalPages, onPageChange, disabled = false, disableNumberedButtons = false, }) => {
|
|
82048
|
+
const getPageNumbers = () => {
|
|
82049
|
+
const pages = [];
|
|
82050
|
+
pages.push(1);
|
|
82051
|
+
let startPage = Math.max(2, currentPage - 1);
|
|
82052
|
+
let endPage = Math.min(totalPages - 1, currentPage + 1);
|
|
82053
|
+
if (currentPage <= 2) {
|
|
82054
|
+
endPage = Math.min(totalPages - 1, 4);
|
|
82055
|
+
}
|
|
82056
|
+
else if (currentPage >= totalPages - 1) {
|
|
82057
|
+
startPage = Math.max(2, totalPages - 3);
|
|
82058
|
+
}
|
|
82059
|
+
if (startPage > 2) {
|
|
82060
|
+
pages.push('ellipsis-start');
|
|
82061
|
+
}
|
|
82062
|
+
for (let i = startPage; i <= endPage; i++) {
|
|
82063
|
+
if (i !== 1 && i !== totalPages) {
|
|
82064
|
+
pages.push(i);
|
|
82065
|
+
}
|
|
82066
|
+
}
|
|
82067
|
+
if (endPage < totalPages - 1) {
|
|
82068
|
+
pages.push('ellipsis-end');
|
|
82069
|
+
}
|
|
82070
|
+
if (totalPages > 1) {
|
|
82071
|
+
pages.push(totalPages);
|
|
82072
|
+
}
|
|
82073
|
+
return pages;
|
|
82074
|
+
};
|
|
82075
|
+
if (totalPages <= 1) {
|
|
82076
|
+
return null;
|
|
82077
|
+
}
|
|
82078
|
+
return (jsx(Pagination, { children: jsxs(PaginationContent, { children: [jsx(PaginationItem, { children: jsx(PaginationPrevious, { size: "sm", onClick: (e) => {
|
|
82079
|
+
e.preventDefault();
|
|
82080
|
+
if (!disabled && currentPage > 1) {
|
|
82081
|
+
onPageChange(currentPage - 1);
|
|
82082
|
+
}
|
|
82083
|
+
}, "aria-disabled": currentPage === 1 || disabled, className: cn('cursor-pointer', (currentPage === 1 || disabled) && 'pointer-events-none opacity-50') }) }), !disableNumberedButtons &&
|
|
82084
|
+
getPageNumbers().map((page, index) => (jsx(PaginationItem, { children: page === 'ellipsis-start' || page === 'ellipsis-end' ? (jsx(PaginationEllipsis, {})) : (jsx(PaginationLink, { size: "sm", onClick: (e) => {
|
|
82085
|
+
e.preventDefault();
|
|
82086
|
+
if (!disabled && typeof page === 'number') {
|
|
82087
|
+
onPageChange(page);
|
|
82088
|
+
}
|
|
82089
|
+
}, isActive: page === currentPage, className: cn('cursor-pointer', disabled && 'pointer-events-none opacity-50'), children: page })) }, index))), jsx(PaginationItem, { children: jsx(PaginationNext, { size: "sm", onClick: (e) => {
|
|
82090
|
+
e.preventDefault();
|
|
82091
|
+
if (!disabled && currentPage < totalPages) {
|
|
82092
|
+
onPageChange(currentPage + 1);
|
|
82093
|
+
}
|
|
82094
|
+
}, "aria-disabled": currentPage === totalPages || disabled, className: cn('cursor-pointer', (currentPage === totalPages || disabled) && 'pointer-events-none opacity-50') }) })] }) }));
|
|
82095
|
+
};
|
|
82096
|
+
|
|
82097
|
+
const MEMORIES_PAGE_SIZE = 25;
|
|
82019
82098
|
function MemoryTab({ org, username }) {
|
|
82020
82099
|
var _a, _b, _c;
|
|
82021
82100
|
const [isAddDialogOpen, setIsAddDialogOpen] = useState(false);
|
|
82022
82101
|
const [deletingId, setDeletingId] = useState(null);
|
|
82023
|
-
|
|
82024
|
-
|
|
82102
|
+
const [currentPage, setCurrentPage] = useState(1);
|
|
82103
|
+
// Platform memsearch status (available to students and admins)
|
|
82104
|
+
const { data: memsearchStatus, isLoading: isLoadingConfig, isError: isConfigError, } = useGetMemsearchStatusQuery({
|
|
82025
82105
|
org,
|
|
82026
82106
|
userId: username,
|
|
82027
82107
|
});
|
|
@@ -82035,8 +82115,9 @@ function MemoryTab({ org, username }) {
|
|
|
82035
82115
|
const { data: memoriesData, isLoading: isLoadingMemories } = useGetGlobalMemoriesQuery({
|
|
82036
82116
|
org,
|
|
82037
82117
|
userId: username,
|
|
82038
|
-
params: { page_size:
|
|
82118
|
+
params: { page: currentPage, page_size: MEMORIES_PAGE_SIZE },
|
|
82039
82119
|
});
|
|
82120
|
+
const totalPages = (memoriesData === null || memoriesData === void 0 ? void 0 : memoriesData.count) ? Math.ceil(memoriesData.count / MEMORIES_PAGE_SIZE) : 0;
|
|
82040
82121
|
const [deleteMemory] = useDeleteGlobalMemoryMutation();
|
|
82041
82122
|
const handleToggleSetting = async (key, checked) => {
|
|
82042
82123
|
try {
|
|
@@ -82072,10 +82153,10 @@ function MemoryTab({ org, username }) {
|
|
|
82072
82153
|
return (jsxs("div", { className: "max-w-2xl space-y-6", children: [jsx(Skeleton, { className: "h-20 w-full rounded-lg" }), jsx(Skeleton, { className: "h-20 w-full rounded-lg" }), jsx(Skeleton, { className: "h-40 w-full rounded-lg" })] }));
|
|
82073
82154
|
}
|
|
82074
82155
|
// Hide memory UI when platform has disabled memsearch (skip check if API errored)
|
|
82075
|
-
if (!isConfigError &&
|
|
82156
|
+
if (!isConfigError && memsearchStatus && !memsearchStatus.enable_memsearch) {
|
|
82076
82157
|
return (jsx("div", { className: "max-w-2xl", children: jsx("div", { className: "rounded-lg border px-6 py-8 text-center", style: { borderColor: 'oklch(.922 0 0)' }, children: jsx("p", { className: "text-sm text-gray-500", children: "The memory feature is not enabled for this platform. Contact your administrator to enable it." }) }) }));
|
|
82077
82158
|
}
|
|
82078
|
-
return (jsxs("div", { className: "max-w-2xl space-y-8", children: [jsxs("div", { className: "space-y-4", children: [jsx("h4", { className: "text-sm font-semibold text-gray-700 dark:text-gray-300 tracking-wide", children: "Memory & Personalization" }), jsxs("div", { className: "flex items-center justify-between rounded-lg border px-6 py-5", style: { borderColor: 'oklch(.922 0 0)' }, children: [jsxs("div", { className: "flex-1 mr-4", children: [jsx("span", { className: "text-sm font-medium text-gray-900 dark:text-gray-100", children: "Allow AI to learn from our conversations" }), jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: "When enabled, the AI will remember useful information from your chats to provide better assistance." })] }), jsx(Switch, { checked: (_a = settings === null || settings === void 0 ? void 0 : settings.auto_capture_enabled) !== null && _a !== void 0 ? _a : false, onCheckedChange: (checked) => handleToggleSetting('auto_capture_enabled', checked), disabled: isUpdatingSettings, "aria-label": `Auto memory capture ${(settings === null || settings === void 0 ? void 0 : settings.auto_capture_enabled) ? 'enabled' : 'disabled'}`, className: "cursor-pointer data-[state=checked]:bg-blue-500 flex-shrink-0" })] }), jsxs("div", { className: "flex items-center justify-between rounded-lg border px-6 py-5", style: { borderColor: 'oklch(.922 0 0)' }, children: [jsxs("div", { className: "flex-1 mr-4", children: [jsx("span", { className: "text-sm font-medium text-gray-900 dark:text-gray-100", children: "Use my saved information in responses" }), jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: "When enabled, the AI will use what it knows about you to personalize responses." })] }), jsx(Switch, { checked: (_b = settings === null || settings === void 0 ? void 0 : settings.use_memory_in_responses) !== null && _b !== void 0 ? _b : false, onCheckedChange: (checked) => handleToggleSetting('use_memory_in_responses', checked), disabled: isUpdatingSettings, "aria-label": `Use memory in responses ${(settings === null || settings === void 0 ? void 0 : settings.use_memory_in_responses) ? 'enabled' : 'disabled'}`, className: "cursor-pointer data-[state=checked]:bg-blue-500 flex-shrink-0" })] })] }), jsxs("div", { className: "space-y-4", children: [jsxs("div", { className: "flex items-center justify-between", children: [jsx("h4", { className: "text-sm font-semibold text-gray-700 dark:text-gray-300 tracking-wide", children: "My Memories" }), jsxs(Button$1, { variant: "outline", size: "sm", onClick: () => setIsAddDialogOpen(true), className: "text-xs", children: [jsx(Plus, { className: "h-3 w-3 mr-1" }), "Add Memory"] })] }), isLoadingMemories ? (jsx("div", { className: "space-y-3", children: [1, 2, 3].map((i) => (jsx(Skeleton, { className: "h-16 w-full rounded-lg" }, i))) })) : ((_c = memoriesData === null || memoriesData === void 0 ? void 0 : memoriesData.results) === null || _c === void 0 ? void 0 : _c.length) ? (jsx("div", { className: "space-y-3", children: memoriesData.results.map((memory) => (jsxs("div", { className: "group flex items-start gap-3 rounded-lg border px-4 py-3", style: { borderColor: 'oklch(.922 0 0)' }, children: [jsx("div", { className: "flex-shrink-0 mt-0.5", children: memory.is_auto_generated ? (jsx(Bot, { className: "h-4 w-4 text-blue-500", "aria-label": "Auto-generated memory" })) : (jsx(User, { className: "h-4 w-4 text-gray-400", "aria-label": "Manually added memory" })) }), jsxs("div", { className: "flex-1 min-w-0", children: [jsx("p", { className: "text-sm text-gray-800 dark:text-gray-200", children: memory.content }), jsxs("p", { className: "text-xs text-gray-400 mt-1", children: [new Date(memory.created_at).toLocaleDateString(), memory.is_auto_generated && (jsx("span", { className: "ml-2 inline-flex items-center rounded-full bg-blue-50 px-1.5 py-0.5 text-[10px] font-medium text-blue-600", children: "auto" }))] })] }), jsx("button", { type: "button", onClick: () => handleDeleteMemory(memory.id), disabled: deletingId === memory.id, className: "flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded hover:bg-gray-100 text-gray-400 hover:text-gray-600 focus:opacity-100 focus:outline-none focus:ring-2 focus:ring-gray-300", "aria-label": `Delete memory: ${memory.content.substring(0, 30)}`, children: deletingId === memory.id ? (jsx("div", { className: "w-4 h-4 border-2 border-gray-500 border-t-transparent rounded-full animate-spin" })) : (jsx(
|
|
82159
|
+
return (jsxs("div", { className: "max-w-2xl space-y-8", children: [jsxs("div", { className: "space-y-4", children: [jsx("h4", { className: "text-sm font-semibold text-gray-700 dark:text-gray-300 tracking-wide", children: "Memory & Personalization" }), jsxs("div", { className: "flex items-center justify-between rounded-lg border px-6 py-5", style: { borderColor: 'oklch(.922 0 0)' }, children: [jsxs("div", { className: "flex-1 mr-4", children: [jsx("span", { className: "text-sm font-medium text-gray-900 dark:text-gray-100", children: "Allow AI to learn from our conversations" }), jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: "When enabled, the AI will remember useful information from your chats to provide better assistance." })] }), jsx(Switch, { checked: (_a = settings === null || settings === void 0 ? void 0 : settings.auto_capture_enabled) !== null && _a !== void 0 ? _a : false, onCheckedChange: (checked) => handleToggleSetting('auto_capture_enabled', checked), disabled: isUpdatingSettings, "aria-label": `Auto memory capture ${(settings === null || settings === void 0 ? void 0 : settings.auto_capture_enabled) ? 'enabled' : 'disabled'}`, className: "cursor-pointer data-[state=checked]:bg-blue-500 flex-shrink-0" })] }), jsxs("div", { className: "flex items-center justify-between rounded-lg border px-6 py-5", style: { borderColor: 'oklch(.922 0 0)' }, children: [jsxs("div", { className: "flex-1 mr-4", children: [jsx("span", { className: "text-sm font-medium text-gray-900 dark:text-gray-100", children: "Use my saved information in responses" }), jsx("p", { className: "text-xs text-gray-500 dark:text-gray-400 mt-1", children: "When enabled, the AI will use what it knows about you to personalize responses." })] }), jsx(Switch, { checked: (_b = settings === null || settings === void 0 ? void 0 : settings.use_memory_in_responses) !== null && _b !== void 0 ? _b : false, onCheckedChange: (checked) => handleToggleSetting('use_memory_in_responses', checked), disabled: isUpdatingSettings, "aria-label": `Use memory in responses ${(settings === null || settings === void 0 ? void 0 : settings.use_memory_in_responses) ? 'enabled' : 'disabled'}`, className: "cursor-pointer data-[state=checked]:bg-blue-500 flex-shrink-0" })] })] }), jsxs("div", { className: "space-y-4", children: [jsxs("div", { className: "flex items-center justify-between", children: [jsx("h4", { className: "text-sm font-semibold text-gray-700 dark:text-gray-300 tracking-wide", children: "My Memories" }), jsxs(Button$1, { variant: "outline", size: "sm", onClick: () => setIsAddDialogOpen(true), className: "text-xs", children: [jsx(Plus, { className: "h-3 w-3 mr-1" }), "Add Memory"] })] }), isLoadingMemories ? (jsx("div", { className: "space-y-3", children: [1, 2, 3].map((i) => (jsx(Skeleton, { className: "h-16 w-full rounded-lg" }, i))) })) : ((_c = memoriesData === null || memoriesData === void 0 ? void 0 : memoriesData.results) === null || _c === void 0 ? void 0 : _c.length) ? (jsx("div", { className: "space-y-3", children: memoriesData.results.map((memory) => (jsxs("div", { className: "group flex items-start gap-3 rounded-lg border px-4 py-3", style: { borderColor: 'oklch(.922 0 0)' }, children: [jsx("div", { className: "flex-shrink-0 mt-0.5", children: memory.is_auto_generated ? (jsx(Bot, { className: "h-4 w-4 text-blue-500", "aria-label": "Auto-generated memory" })) : (jsx(User, { className: "h-4 w-4 text-gray-400", "aria-label": "Manually added memory" })) }), jsxs("div", { className: "flex-1 min-w-0", children: [jsx("p", { className: "text-sm text-gray-800 dark:text-gray-200", children: memory.content }), jsxs("p", { className: "text-xs text-gray-400 mt-1", children: [new Date(memory.created_at).toLocaleDateString(), memory.is_auto_generated && (jsx("span", { className: "ml-2 inline-flex items-center rounded-full bg-blue-50 px-1.5 py-0.5 text-[10px] font-medium text-blue-600", children: "auto" }))] })] }), jsx("button", { type: "button", onClick: () => handleDeleteMemory(memory.id), disabled: deletingId === memory.id, className: "flex-shrink-0 opacity-0 group-hover:opacity-100 transition-opacity p-1 rounded hover:bg-gray-100 text-gray-400 hover:text-gray-600 focus:opacity-100 focus:outline-none focus:ring-2 focus:ring-gray-300", "aria-label": `Delete memory: ${memory.content.substring(0, 30)}`, children: deletingId === memory.id ? (jsx("div", { className: "w-4 h-4 border-2 border-gray-500 border-t-transparent rounded-full animate-spin" })) : (jsx(Trash2, { className: "h-4 w-4" })) })] }, memory.id))) })) : (jsx("div", { className: "rounded-lg border px-6 py-8 text-center", style: { borderColor: 'oklch(.922 0 0)' }, children: jsx("p", { className: "text-sm text-gray-500", children: "No memories yet. The AI will start remembering information from your conversations, or you can add memories manually." }) })), totalPages > 1 && (jsx(IblPagination, { currentPage: currentPage, totalPages: totalPages, onPageChange: setCurrentPage, disabled: isLoadingMemories }))] }), jsx(AddMemoryDialog, { open: isAddDialogOpen, onOpenChange: setIsAddDialogOpen, org: org, username: username })] }));
|
|
82079
82160
|
}
|
|
82080
82161
|
|
|
82081
82162
|
const renderLucideIcon = (Icon) => function RenderedIcon(props) {
|
|
@@ -82089,17 +82170,17 @@ function Profile({ tenant, username, onClose, customization = {}, isAdmin = fals
|
|
|
82089
82170
|
isUsingFoundry: localLLMProps === null || localLLMProps === void 0 ? void 0 : localLLMProps.isUsingFoundry,
|
|
82090
82171
|
hasFoundryStatus: 'foundryStatus' in (localLLMProps || {}),
|
|
82091
82172
|
});
|
|
82092
|
-
const { data:
|
|
82093
|
-
console.log('[Profile] memsearch
|
|
82173
|
+
const { data: memsearchStatus, isError: isMemsearchStatusError, isLoading: isMemsearchStatusLoading, error: memsearchStatusError, } = useGetMemsearchStatusQuery({ org: tenant, userId: username }, { skip: !enableMemoryTab });
|
|
82174
|
+
console.log('[Profile] memsearch status debug:', {
|
|
82094
82175
|
enableMemoryTab,
|
|
82095
|
-
|
|
82096
|
-
|
|
82097
|
-
|
|
82098
|
-
|
|
82099
|
-
enable_memsearch:
|
|
82100
|
-
});
|
|
82101
|
-
// Show memory tab if enabled for this platform AND memsearch is enabled (or
|
|
82102
|
-
const isMemoryEnabled = enableMemoryTab && (
|
|
82176
|
+
isMemsearchStatusLoading,
|
|
82177
|
+
isMemsearchStatusError,
|
|
82178
|
+
memsearchStatusError,
|
|
82179
|
+
memsearchStatus,
|
|
82180
|
+
enable_memsearch: memsearchStatus === null || memsearchStatus === void 0 ? void 0 : memsearchStatus.enable_memsearch,
|
|
82181
|
+
});
|
|
82182
|
+
// Show memory tab if enabled for this platform AND memsearch is enabled (or status API is unavailable)
|
|
82183
|
+
const isMemoryEnabled = enableMemoryTab && (isMemsearchStatusError || ((_a = memsearchStatus === null || memsearchStatus === void 0 ? void 0 : memsearchStatus.enable_memsearch) !== null && _a !== void 0 ? _a : false));
|
|
82103
82184
|
const baseTabs = [
|
|
82104
82185
|
{ id: 'basic', label: 'Basic', renderIcon: renderLucideIcon(User) },
|
|
82105
82186
|
{ id: 'social', label: 'Social', renderIcon: renderLucideIcon(Globe) },
|
|
@@ -82662,7 +82743,7 @@ TabsTrigger.displayName = Trigger$3.displayName;
|
|
|
82662
82743
|
const TabsContent = React.forwardRef(({ className, ...props }, ref) => (jsx(Content$1, { ref: ref, className: cn("ring-offset-background focus-visible:ring-ring mt-2 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none", className), ...props })));
|
|
82663
82744
|
TabsContent.displayName = Content$1.displayName;
|
|
82664
82745
|
|
|
82665
|
-
const getUserName = () => {
|
|
82746
|
+
const getUserName$1 = () => {
|
|
82666
82747
|
var _a;
|
|
82667
82748
|
return (_a = JSON.parse(localStorage.getItem("userData"))) === null || _a === void 0 ? void 0 : _a.user_nicename;
|
|
82668
82749
|
};
|
|
@@ -82829,7 +82910,7 @@ function UsersTab({ tenant, currentPage, itemsPerPage, onInviteSuccess }) {
|
|
|
82829
82910
|
email,
|
|
82830
82911
|
platform_key: tenant,
|
|
82831
82912
|
redirect_to: window.location.origin + window.location.pathname,
|
|
82832
|
-
source: getUserName(),
|
|
82913
|
+
source: getUserName$1(),
|
|
82833
82914
|
active: true,
|
|
82834
82915
|
},
|
|
82835
82916
|
}).unwrap();
|
|
@@ -82909,7 +82990,7 @@ function UsersTab({ tenant, currentPage, itemsPerPage, onInviteSuccess }) {
|
|
|
82909
82990
|
email: emailValue,
|
|
82910
82991
|
platform_key: platformKey || tenant,
|
|
82911
82992
|
redirect_to: window.location.origin + window.location.pathname,
|
|
82912
|
-
source: getUserName(),
|
|
82993
|
+
source: getUserName$1(),
|
|
82913
82994
|
active: true,
|
|
82914
82995
|
...(companyNameIndex !== -1 &&
|
|
82915
82996
|
((_c = row[companyNameIndex]) === null || _c === void 0 ? void 0 : _c.trim()) && {
|
|
@@ -83091,7 +83172,7 @@ function CoursesTab({ tenant, currentPage, itemsPerPage, hasManageUsersPermissio
|
|
|
83091
83172
|
// Search query for courses
|
|
83092
83173
|
const { data: coursesData, isLoading: isLoadingCourses, isFetching: isFetchingCourses, } = useGetPersonnalizedSearchQuery([
|
|
83093
83174
|
{
|
|
83094
|
-
username: getUserName(),
|
|
83175
|
+
username: getUserName$1(),
|
|
83095
83176
|
content: ['courses'],
|
|
83096
83177
|
query: debouncedCourseSearch || '',
|
|
83097
83178
|
returnFacet: false,
|
|
@@ -83467,7 +83548,7 @@ function ProgramsTab({ tenant, currentPage, itemsPerPage, hasManageUsersPermissi
|
|
|
83467
83548
|
// Search query for programs
|
|
83468
83549
|
const { data: programsData, isLoading: isLoadingPrograms, isFetching: isFetchingPrograms, } = useGetPersonnalizedSearchQuery([
|
|
83469
83550
|
{
|
|
83470
|
-
username: getUserName(),
|
|
83551
|
+
username: getUserName$1(),
|
|
83471
83552
|
content: ['programs'],
|
|
83472
83553
|
query: debouncedProgramSearch || '',
|
|
83473
83554
|
returnFacet: false,
|
|
@@ -122438,7 +122519,7 @@ function EditAlertDialog({ open, onOpenChange, template, platformKey, templateTy
|
|
|
122438
122519
|
}, [currentTemplateDetail, isHumanSupport, template]);
|
|
122439
122520
|
useEffect(() => {
|
|
122440
122521
|
try {
|
|
122441
|
-
const storedUserName = getUserName();
|
|
122522
|
+
const storedUserName = getUserName$1();
|
|
122442
122523
|
if (storedUserName) {
|
|
122443
122524
|
setUsername(storedUserName);
|
|
122444
122525
|
}
|
|
@@ -186823,6 +186904,2490 @@ function AnalyticsMonetizationStats({ tenantKey }) {
|
|
|
186823
186904
|
setPaywallPage(paywallsData.previous_page), children: "Previous" }), jsxs("span", { className: "text-xs text-gray-500", children: ["Page ", paywallPage] }), jsx(Button$1, { variant: "outline", size: "sm", className: "h-8 text-xs", disabled: paywallsData.next_page === null, onClick: () => paywallsData.next_page !== null && setPaywallPage(paywallsData.next_page), children: "Next" })] })] }))] }) })] }));
|
|
186824
186905
|
}
|
|
186825
186906
|
|
|
186907
|
+
const SkeletonMultiplier = ({ Skeleton, multiplier }) => {
|
|
186908
|
+
return (jsx(Fragment$1, { children: Array.from({ length: multiplier }).map((_, index) => (jsx(Skeleton, {}, index))) }));
|
|
186909
|
+
};
|
|
186910
|
+
|
|
186911
|
+
const SkeletonActivityStatBox = () => {
|
|
186912
|
+
return (jsxs("div", { className: "flex flex-col items-center justify-center rounded-lg border border-gray-100 bg-gray-50 p-3 transition-shadow hover:shadow-sm", children: [jsx("div", { className: "mb-1 h-6 w-12 animate-pulse rounded bg-gray-200" }), jsx("div", { className: "h-3 w-16 animate-pulse rounded bg-gray-200" })] }));
|
|
186913
|
+
};
|
|
186914
|
+
|
|
186915
|
+
function CourseCardSkeleton() {
|
|
186916
|
+
return (jsxs("div", { className: "relative flex h-full w-full flex-col overflow-hidden rounded-md border border-gray-200 bg-white shadow-sm", children: [jsx("div", { className: "animate-shimmer pointer-events-none absolute inset-0 z-10" }), jsx("div", { className: "skeleton-bg relative aspect-video w-full" }), jsxs("div", { className: "flex flex-1 flex-col justify-between p-4", children: [jsxs("div", { children: [jsx("div", { className: "skeleton-bg mb-2 h-4 w-24 rounded-sm" }), jsx("div", { className: "skeleton-bg mb-2 h-4 w-full rounded-sm" }), jsx("div", { className: "skeleton-bg h-4 w-3/4 rounded-sm" })] }), jsxs("div", { className: "mt-4 flex items-center justify-between", children: [jsx("div", { className: "skeleton-bg h-8 w-8 rounded-sm" }), jsx("div", { className: "skeleton-bg h-8 w-8 rounded-sm" })] })] })] }));
|
|
186917
|
+
}
|
|
186918
|
+
|
|
186919
|
+
const DefaultEmptyBox = ({ image = '/images/empty-data-icon.svg', message = 'No data available', imageSize = 40, bordered = true, className = '', }) => {
|
|
186920
|
+
return (jsx("div", { className: `border ${bordered ? 'border-gray-200' : 'border-none'} rounded-lg p-12 ${className}`, children: jsxs("div", { className: "flex flex-col items-center justify-center text-center", children: [jsx("div", { className: "mb-4 flex h-12 w-12 items-center justify-center", children: jsx("img", { src: image, alt: message, width: imageSize, height: imageSize, className: "h-10 w-10" }) }), jsx("p", { className: "text-gray-600", children: message })] }) }));
|
|
186921
|
+
};
|
|
186922
|
+
|
|
186923
|
+
const CredentialMiniBoxSkeleton = () => {
|
|
186924
|
+
return (jsxs("div", { className: "flex animate-pulse items-start rounded-lg border border-gray-200 bg-white p-4", children: [jsx("div", { className: "mr-4 h-12 w-12 flex-shrink-0 rounded-full bg-gray-200" }), jsxs("div", { className: "flex-1", children: [jsx("div", { className: "mb-2 h-4 w-3/4 rounded bg-gray-200" }), jsx("div", { className: "mb-2 h-3 w-1/2 rounded bg-gray-200" }), jsx("div", { className: "h-3 w-2/3 rounded bg-gray-200" })] })] }));
|
|
186925
|
+
};
|
|
186926
|
+
|
|
186927
|
+
const SkillBox = ({ skill, onSkillClick, showRating = true, iconSrc = '/images/empty-data-icon.svg', }) => {
|
|
186928
|
+
return (jsxs("div", { className: "flex cursor-pointer flex-col items-center rounded-lg border border-gray-200 p-6 transition-all hover:shadow-md", onClick: () => onSkillClick === null || onSkillClick === void 0 ? void 0 : onSkillClick(skill), children: [jsx("div", { className: "mb-2", children: jsx("img", { src: iconSrc, alt: "Skills icon", width: 32, height: 32, className: "h-8 w-8" }) }), jsx("p", { className: "mb-4 text-center text-sm text-gray-600", children: skill.name }), showRating && (jsxs("div", { className: "relative mb-4 h-20 w-20", children: [jsxs("svg", { className: "h-full w-full", viewBox: "0 0 100 100", children: [jsx("circle", { cx: "50", cy: "50", r: "40", fill: "none", stroke: "#E5E7EB", strokeWidth: "10" }), jsx("circle", { cx: "50", cy: "50", r: "40", fill: "none", stroke: "#F8B43A", strokeWidth: "10", strokeDasharray: "251.2", strokeDashoffset: (100 - ((skill.level || 1) / 5) * 100) * 2.512, transform: "rotate(-90 50 50)" })] }), jsx("div", { className: "absolute inset-0 flex items-center justify-center", children: jsx("span", { className: "text-2xl font-bold text-gray-700", children: skill.level || 1 }) })] })), jsx(Star$1, { className: "h-6 w-6 fill-amber-500 text-amber-500" })] }));
|
|
186929
|
+
};
|
|
186930
|
+
|
|
186931
|
+
const SkeletonSkillBox = () => {
|
|
186932
|
+
return (jsxs("div", { className: "flex w-[200px] flex-shrink-0 animate-pulse flex-col items-center rounded-lg border border-gray-200 p-6", children: [jsx("div", { className: "mb-2", children: jsx("div", { className: "h-8 w-8 rounded-full bg-gray-200" }) }), jsx("div", { className: "mb-4 h-4 w-20 rounded bg-gray-200" }), jsxs("div", { className: "relative mb-4 h-20 w-20", children: [jsx("svg", { className: "h-full w-full", viewBox: "0 0 100 100", children: jsx("circle", { cx: "50", cy: "50", r: "40", fill: "none", stroke: "#E5E7EB", strokeWidth: "10" }) }), jsx("div", { className: "absolute inset-0 flex items-center justify-center", children: jsx("div", { className: "h-8 w-8 rounded-full bg-gray-200" }) })] }), jsx(Star$1, { className: "h-6 w-6 text-gray-200" })] }));
|
|
186933
|
+
};
|
|
186934
|
+
|
|
186935
|
+
const RATING_DESCRIPTIONS = {
|
|
186936
|
+
1: 'Basic understanding of fundamentals; requires significant guidance.',
|
|
186937
|
+
2: 'Familiar with core concepts; can complete routine tasks with some supervision.',
|
|
186938
|
+
3: 'Capable of managing varied tasks; understands nuances but may seek guidance for complexities.',
|
|
186939
|
+
4: 'Proficient with advanced concepts; can work independently on complex tasks.',
|
|
186940
|
+
5: 'Expert level mastery; can innovate and teach others in this domain.',
|
|
186941
|
+
};
|
|
186942
|
+
function SkillDetailModal({ skill, updatingSkill = false, deletingSkill = false, onClose, onRatingChange, onDeleteSkill, }) {
|
|
186943
|
+
const [originalRating] = useState(skill.rating || 1);
|
|
186944
|
+
const [tempRating, setTempRating] = useState(skill.rating || 1);
|
|
186945
|
+
const handleTempRatingChange = (rating) => {
|
|
186946
|
+
setTempRating(rating);
|
|
186947
|
+
};
|
|
186948
|
+
const handleConfirm = () => {
|
|
186949
|
+
if (updatingSkill)
|
|
186950
|
+
return;
|
|
186951
|
+
if (onRatingChange) {
|
|
186952
|
+
onRatingChange(tempRating);
|
|
186953
|
+
}
|
|
186954
|
+
};
|
|
186955
|
+
const handleCancel = () => {
|
|
186956
|
+
onClose();
|
|
186957
|
+
};
|
|
186958
|
+
return (jsx("div", { className: "bg-opacity-50 fixed inset-0 z-50 flex items-center justify-center bg-black p-4", children: jsxs("div", { className: "max-h-[85vh] w-full max-w-md overflow-y-auto rounded-lg bg-white", children: [jsxs("div", { className: "flex items-center justify-between border-b border-gray-200 bg-gradient-to-r from-[var(--background-light)] to-[var(--primary-light)]/30 p-4", children: [jsxs("h3", { className: "text-lg font-medium text-[var(--text)]", children: ["Rate your expertise in \"", skill.name, "\""] }), jsx("button", { onClick: handleCancel, className: "rounded-full p-1 text-gray-400 hover:bg-[var(--primary-light)] hover:text-gray-500", "aria-label": "Close skill details", children: jsx(X, { className: "h-5 w-5" }) })] }), jsxs("div", { className: "max-h-[70vh] overflow-y-auto p-6", style: { scrollbarWidth: 'none', msOverflowStyle: 'none' }, children: [jsxs("div", { className: "mb-8", children: [jsxs("div", { className: "relative mb-10 pt-3", children: [jsx("div", { className: "h-2 rounded-full bg-gradient-to-r from-amber-100 to-amber-200" }), [1, 2, 3, 4, 5].map((rating) => {
|
|
186959
|
+
const position = ((rating - 1) / 4) * 100;
|
|
186960
|
+
return (jsx("div", { onClick: () => handleTempRatingChange(rating), className: `absolute top-3 -mt-1 -ml-2 h-4 w-4 cursor-pointer rounded-full border-2 transition-all duration-200 ${rating <= tempRating
|
|
186961
|
+
? 'border-white bg-amber-500'
|
|
186962
|
+
: 'border-amber-200 bg-white'}`, style: { left: `${position}%` } }, rating));
|
|
186963
|
+
}), jsx("div", { className: "absolute top-3 left-0 h-2 rounded-full bg-gradient-to-r from-amber-400 to-amber-500 transition-all duration-200", style: {
|
|
186964
|
+
width: tempRating === 1 ? '0%' : `${((tempRating - 1) / 4) * 100}%`,
|
|
186965
|
+
} }), [1, 2, 3, 4, 5].map((rating) => {
|
|
186966
|
+
const position = ((rating - 1) / 4) * 100;
|
|
186967
|
+
return (jsx("div", { onClick: () => handleTempRatingChange(rating), className: "absolute top-7 -ml-2 w-4 cursor-pointer text-center transition-all duration-200", style: { left: `${position}%` }, children: jsx("span", { className: `text-sm font-medium ${rating === tempRating ? 'text-amber-600' : 'text-gray-500'}`, children: rating }) }, `label-${rating}`));
|
|
186968
|
+
}), jsx("div", { className: "absolute top-3 -mt-2 -ml-3 h-6 w-6 rounded-full border-2 border-white bg-amber-500 shadow-md transition-all duration-200", style: { left: `${((tempRating - 1) / 4) * 100}%` } }), jsx("input", { type: "range", min: 1, max: 5, step: 1, value: tempRating, onChange: (e) => handleTempRatingChange(Number.parseInt(e.target.value)), className: "absolute top-2 z-10 h-4 w-full cursor-pointer opacity-0", style: { margin: 0, padding: 0 }, "aria-label": "Skill rating" })] }), jsxs("div", { className: "mt-4 flex justify-between text-sm font-medium", children: [jsx("span", { className: "text-gray-600", children: "Beginner" }), jsxs("span", { className: "text-amber-600", children: ["Level ", tempRating] }), jsx("span", { className: "text-gray-600", children: "Expert" })] })] }), jsxs("div", { className: "mb-6 flex flex-col items-center", children: [jsxs("div", { className: "relative mb-4 h-20 w-20", children: [jsxs("svg", { className: "h-full w-full", viewBox: "0 0 100 100", children: [jsx("circle", { cx: "50", cy: "50", r: "40", fill: "none", stroke: "#E5E7EB", strokeWidth: "10" }), jsx("circle", { cx: "50", cy: "50", r: "40", fill: "none", stroke: "#F8B43A", strokeWidth: "10", strokeDasharray: "251.2", strokeDashoffset: `${(100 - ((tempRating || 1) / 5) * 100) * 2.512}`, transform: "rotate(-90 50 50)" })] }), jsx("div", { className: "absolute inset-0 flex items-center justify-center", children: jsx("span", { className: "text-3xl font-bold text-gray-700", children: tempRating }) })] }), jsx("p", { className: "max-w-xs text-center text-sm text-gray-600", children: RATING_DESCRIPTIONS[tempRating] })] })] }), jsxs("div", { className: "flex justify-between border-t border-gray-200 bg-gradient-to-r from-[var(--background-light)] to-[var(--primary-light)]/30 p-4", children: [jsxs("div", { className: "flex gap-2", children: [jsx("button", { onClick: handleCancel, className: "rounded-md border border-gray-300 px-4 py-2 text-sm font-medium text-gray-700 hover:bg-gray-50", children: "Cancel" }), onDeleteSkill && (jsx("button", { onClick: onDeleteSkill, className: "rounded-md border border-gray-300 px-4 py-2 text-sm font-medium text-gray-600 hover:bg-gray-50", children: deletingSkill ? 'Deleting...' : 'Delete skill' }))] }), jsx("button", { onClick: handleConfirm, className: `rounded-md bg-gradient-to-r from-[var(--button-primary-gradient-from)] to-[var(--button-primary-gradient-to)] px-4 py-2 text-sm font-medium text-[var(--button-primary-text)] transition-all ${tempRating !== originalRating
|
|
186969
|
+
? 'animate-pulse hover:opacity-[var(--button-primary-hover-opacity)]'
|
|
186970
|
+
: 'hover:opacity-[var(--button-primary-hover-opacity)]'}`, children: updatingSkill ? 'Updating...' : 'Confirm' })] })] }) }));
|
|
186971
|
+
}
|
|
186972
|
+
|
|
186973
|
+
const SkeletonPathwayBox = () => {
|
|
186974
|
+
return (jsxs("div", { className: "h-[200px] animate-pulse overflow-hidden rounded-lg border border-gray-200 bg-white", children: [jsx("div", { className: "h-32 bg-gray-200" }), jsxs("div", { className: "p-4", children: [jsx("div", { className: "mb-4 h-4 w-3/4 rounded bg-gray-200" }), jsx("div", { className: "mb-1 h-2 w-full rounded bg-gray-200" }), jsx("div", { className: "h-2 w-1/4 rounded bg-gray-200" })] })] }));
|
|
186975
|
+
};
|
|
186976
|
+
|
|
186977
|
+
/**
|
|
186978
|
+
* Skeleton placeholder for a single profile info card (Last Accessed,
|
|
186979
|
+
* Joined, Total Time Spent, Top Content). Ported from the skillsai SPA
|
|
186980
|
+
* (components/skeleton-profile-info-card.tsx).
|
|
186981
|
+
*/
|
|
186982
|
+
const SkeletonProfileInfoCard = () => (jsxs("div", { className: "flex items-start rounded-sm border border-[var(--border)] p-4", children: [jsx(Skeleton, { className: "mr-4 h-12 w-12 rounded-sm" }), jsxs("div", { children: [jsx(Skeleton, { className: "mb-2 h-4 w-24" }), jsx(Skeleton, { className: "h-5 w-32" })] })] }));
|
|
186983
|
+
|
|
186984
|
+
/**
|
|
186985
|
+
* Browser-only helpers used by skills-profile hooks and components.
|
|
186986
|
+
*
|
|
186987
|
+
* These are sync localStorage accessors ported from the skillsai SPA. They
|
|
186988
|
+
* intentionally live here (not in @iblai/web-utils) to avoid colliding with
|
|
186989
|
+
* the existing async `getUserName(storageService)` helper in
|
|
186990
|
+
* packages/web-utils/src/providers/auth-provider.tsx, and because no other
|
|
186991
|
+
* package consumes them.
|
|
186992
|
+
*/
|
|
186993
|
+
const LS_KEYS = {
|
|
186994
|
+
TENANT: 'tenant',
|
|
186995
|
+
CURRENT_TENANT: 'current_tenant',
|
|
186996
|
+
USER_DATA: 'userData',
|
|
186997
|
+
};
|
|
186998
|
+
const isJSON = (text) => {
|
|
186999
|
+
if (typeof text !== 'string')
|
|
187000
|
+
return false;
|
|
187001
|
+
try {
|
|
187002
|
+
JSON.parse(text);
|
|
187003
|
+
return true;
|
|
187004
|
+
}
|
|
187005
|
+
catch (_a) {
|
|
187006
|
+
return false;
|
|
187007
|
+
}
|
|
187008
|
+
};
|
|
187009
|
+
const readLocalStorage = (key) => {
|
|
187010
|
+
if (typeof window === 'undefined')
|
|
187011
|
+
return null;
|
|
187012
|
+
try {
|
|
187013
|
+
return window.localStorage.getItem(key);
|
|
187014
|
+
}
|
|
187015
|
+
catch (_a) {
|
|
187016
|
+
return null;
|
|
187017
|
+
}
|
|
187018
|
+
};
|
|
187019
|
+
/** Returns the current tenant slug from localStorage, or empty string. */
|
|
187020
|
+
function getTenant() {
|
|
187021
|
+
return readLocalStorage(LS_KEYS.TENANT) || '';
|
|
187022
|
+
}
|
|
187023
|
+
/** Returns the `org` field of the currently selected tenant, or null. */
|
|
187024
|
+
function getOrg() {
|
|
187025
|
+
var _a;
|
|
187026
|
+
const currentTenant = readLocalStorage(LS_KEYS.CURRENT_TENANT);
|
|
187027
|
+
return currentTenant && isJSON(currentTenant) ? (_a = JSON.parse(currentTenant)) === null || _a === void 0 ? void 0 : _a.org : null;
|
|
187028
|
+
}
|
|
187029
|
+
/** Returns the logged-in user's numeric user_id from localStorage, or null. */
|
|
187030
|
+
function getUserId() {
|
|
187031
|
+
var _a;
|
|
187032
|
+
const userData = readLocalStorage(LS_KEYS.USER_DATA);
|
|
187033
|
+
return userData && isJSON(userData) ? (_a = JSON.parse(userData)) === null || _a === void 0 ? void 0 : _a.user_id : null;
|
|
187034
|
+
}
|
|
187035
|
+
/** Returns the logged-in user's nicename (username) from localStorage, or null. */
|
|
187036
|
+
function getUserName() {
|
|
187037
|
+
var _a;
|
|
187038
|
+
const userData = readLocalStorage(LS_KEYS.USER_DATA);
|
|
187039
|
+
return userData && isJSON(userData) ? (_a = JSON.parse(userData)) === null || _a === void 0 ? void 0 : _a.user_nicename : null;
|
|
187040
|
+
}
|
|
187041
|
+
/**
|
|
187042
|
+
* Returns true if the current document is rendered inside an iframe. Safely
|
|
187043
|
+
* handles cross-origin access errors (throw → treated as embedded) and the
|
|
187044
|
+
* server-rendering case (no window → treated as embedded).
|
|
187045
|
+
*/
|
|
187046
|
+
function inIframe() {
|
|
187047
|
+
try {
|
|
187048
|
+
return typeof window !== 'undefined' && window.self !== window.top;
|
|
187049
|
+
}
|
|
187050
|
+
catch (_a) {
|
|
187051
|
+
return true;
|
|
187052
|
+
}
|
|
187053
|
+
}
|
|
187054
|
+
/** Returns a path to a random bundled course placeholder image. */
|
|
187055
|
+
function getRandomCourseImage() {
|
|
187056
|
+
const courseImages = [
|
|
187057
|
+
'/images/courses/c1s.jpeg',
|
|
187058
|
+
'/images/courses/c2s.jpeg',
|
|
187059
|
+
'/images/courses/c3s.jpeg',
|
|
187060
|
+
'/images/courses/c4s.jpeg',
|
|
187061
|
+
'/images/courses/c5s.jpeg',
|
|
187062
|
+
'/images/courses/c6s.jpeg',
|
|
187063
|
+
'/images/courses/c7s.jpeg',
|
|
187064
|
+
'/images/courses/c8s.jpeg',
|
|
187065
|
+
];
|
|
187066
|
+
return courseImages[Math.floor(Math.random() * courseImages.length)];
|
|
187067
|
+
}
|
|
187068
|
+
/**
|
|
187069
|
+
* Triggers the browser's print dialog scoped to a specific DOM element.
|
|
187070
|
+
* Injects a transient @media print stylesheet and cleans it up afterward.
|
|
187071
|
+
*/
|
|
187072
|
+
function inBrowserPrint(layoutToPrint) {
|
|
187073
|
+
if (!layoutToPrint)
|
|
187074
|
+
return;
|
|
187075
|
+
layoutToPrint.classList.add('in-browser-printable');
|
|
187076
|
+
const style = document.createElement('style');
|
|
187077
|
+
style.innerHTML = `
|
|
187078
|
+
@media print {
|
|
187079
|
+
body * { visibility: hidden; }
|
|
187080
|
+
.in-browser-printable, .in-browser-printable * { visibility: visible; }
|
|
187081
|
+
.in-browser-printable {
|
|
187082
|
+
position: absolute;
|
|
187083
|
+
left: 0;
|
|
187084
|
+
top: 0;
|
|
187085
|
+
width: 100%;
|
|
187086
|
+
}
|
|
187087
|
+
}
|
|
187088
|
+
`;
|
|
187089
|
+
document.head.appendChild(style);
|
|
187090
|
+
window.print();
|
|
187091
|
+
document.head.removeChild(style);
|
|
187092
|
+
layoutToPrint.classList.remove('in-browser-printable');
|
|
187093
|
+
}
|
|
187094
|
+
|
|
187095
|
+
/**
|
|
187096
|
+
* Thin wrapper around useGetUserMetadataQuery that reads the username
|
|
187097
|
+
* from localStorage and skips the request when there is no logged-in
|
|
187098
|
+
* user.
|
|
187099
|
+
*
|
|
187100
|
+
* Ported from the skillsai SPA (hooks/users/use-usermetadata.tsx).
|
|
187101
|
+
*/
|
|
187102
|
+
const useUserMetadata = () => {
|
|
187103
|
+
const username = getUserName();
|
|
187104
|
+
const { data: userMetaData, isLoading: userMetaDataLoading, isError: userMetaDataError, } = useGetUserMetadataQuery({ params: { username } }, { skip: !username });
|
|
187105
|
+
return { userMetaData, userMetaDataLoading, userMetaDataError };
|
|
187106
|
+
};
|
|
187107
|
+
|
|
187108
|
+
/**
|
|
187109
|
+
* Presentational avatar for the logged-in user. Pulls the user metadata
|
|
187110
|
+
* via the shared useUserMetadata hook and picks the best available
|
|
187111
|
+
* image source (uploaded profile image → Gravatar → initials fallback).
|
|
187112
|
+
*
|
|
187113
|
+
* Ported from the skillsai SPA (components/header/profile/user-avatar.tsx).
|
|
187114
|
+
* Note: `enableGravatarOnProfilePic` config is now passed in as a prop
|
|
187115
|
+
* rather than read from a skillsai-local config module.
|
|
187116
|
+
*/
|
|
187117
|
+
const UserAvatar = ({ size = 32, containerClassName = '', enableGravatar = true, }) => {
|
|
187118
|
+
var _a, _b, _c, _d;
|
|
187119
|
+
const { userMetaData, userMetaDataLoading } = useUserMetadata();
|
|
187120
|
+
const data = userMetaData;
|
|
187121
|
+
const hasImage = !!((_a = data === null || data === void 0 ? void 0 : data.profile_image) === null || _a === void 0 ? void 0 : _a.has_image);
|
|
187122
|
+
return (jsx(Avatar, { className: containerClassName, children: userMetaDataLoading ? (jsx(Skeleton, { className: "h-8 w-8 rounded-full" })) : (jsxs(Fragment$1, { children: [hasImage ? (jsx(AvatarImage, { src: (_b = data === null || data === void 0 ? void 0 : data.profile_image) === null || _b === void 0 ? void 0 : _b.image_url_large, alt: (_c = data === null || data === void 0 ? void 0 : data.name) !== null && _c !== void 0 ? _c : '', className: "w-full" })) : enableGravatar ? (jsx(Gravatar$1, { className: "w-full", email: (_d = data === null || data === void 0 ? void 0 : data.email) !== null && _d !== void 0 ? _d : '', size: size })) : null, jsx(AvatarFallback, { className: cn(enableGravatar && !hasImage ? 'hidden' : ''), children: getInitials((data === null || data === void 0 ? void 0 : data.name) || (data === null || data === void 0 ? void 0 : data.username) || (data === null || data === void 0 ? void 0 : data.email) || '') })] })) }));
|
|
187123
|
+
};
|
|
187124
|
+
|
|
187125
|
+
/**
|
|
187126
|
+
* Presentational 7-day time-spent bar chart. Callers supply the data
|
|
187127
|
+
* (typically via useProfileTimeSpent) and a loading flag; this component
|
|
187128
|
+
* does no data fetching.
|
|
187129
|
+
*
|
|
187130
|
+
* Ported from the skillsai SPA (components/profile-time-chart.tsx),
|
|
187131
|
+
* refactored to be presentational.
|
|
187132
|
+
*/
|
|
187133
|
+
const ProfileTimeChart = ({ data, loading = false }) => {
|
|
187134
|
+
const renderLegend = (props) => {
|
|
187135
|
+
var _a;
|
|
187136
|
+
const payload = (_a = props.payload) !== null && _a !== void 0 ? _a : [];
|
|
187137
|
+
return (jsx("div", { className: "flex w-full justify-center pt-0 pb-4", children: payload.map((entry, index) => (jsxs("div", { className: "mx-2 flex items-center", children: [jsx("div", { className: "mr-2 h-3 w-3 bg-amber-500 opacity-70" }), jsx("span", { className: "text-xs text-gray-600", children: entry.value })] }, `item-${index}`))) }));
|
|
187138
|
+
};
|
|
187139
|
+
return (jsx("div", { className: "mb-4 border-b border-gray-200 pb-2", children: loading ? (jsx("div", { "data-testid": "profile-time-chart-loading", className: "flex h-64 items-center justify-center", children: jsx("div", { className: "h-8 w-8 animate-spin rounded-full border-4 border-amber-500 border-t-transparent" }) })) : (jsx("div", { className: "relative h-64", children: jsx(ResponsiveContainer, { width: "100%", height: "100%", children: jsxs(BarChart, { data: data, margin: { top: 10, right: 0, left: 0, bottom: 20 }, children: [jsx(CartesianGrid, { strokeDasharray: "3 3" }), jsx(XAxis, { dataKey: "date", axisLine: true, tickLine: true, tick: { fill: '#9CA3AF', fontSize: 12 }, height: 50 }), jsx(YAxis, { axisLine: true, tickLine: true, tick: { fill: '#9CA3AF', fontSize: 12 }, label: {
|
|
187140
|
+
value: 'Minutes',
|
|
187141
|
+
angle: -90,
|
|
187142
|
+
position: 'insideLeft',
|
|
187143
|
+
style: { textAnchor: 'middle', fill: '#9CA3AF', fontSize: 12 },
|
|
187144
|
+
}, domain: [0, 30], ticks: [0, 10, 20, 30, 40, 50] }), jsx(Legend, { content: renderLegend, verticalAlign: "top", height: 36 }), jsx(Bar, { dataKey: "minutes", name: "Minutes", barSize: 130, children: data === null || data === void 0 ? void 0 : data.map((_entry, index) => (jsx(Cell, { fill: "rgba(245, 158, 11, 0.7)" }, `cell-${index}`))) })] }) }) })) }));
|
|
187145
|
+
};
|
|
187146
|
+
|
|
187147
|
+
const skillLevels = [
|
|
187148
|
+
{ name: 'Beginner', value: 0, description: 'basic knowledge' },
|
|
187149
|
+
{ name: 'Novice', value: 100, description: 'limited experience' },
|
|
187150
|
+
{ name: 'Intermediate', value: 200, description: 'practical application' },
|
|
187151
|
+
{ name: 'Advanced', value: 300, description: 'applied theory' },
|
|
187152
|
+
{ name: 'Expert', value: 400, description: 'recognized authority' },
|
|
187153
|
+
];
|
|
187154
|
+
const generateCurveData = () => {
|
|
187155
|
+
const data = [];
|
|
187156
|
+
for (let i = 0; i <= 100; i += 5) {
|
|
187157
|
+
let value = i < 80 ? Math.pow(i, 1.5) * 3 : Math.pow(i, 1.8) * 1.5;
|
|
187158
|
+
value = Math.min(value, 500);
|
|
187159
|
+
let level = 'Beginner';
|
|
187160
|
+
let description = 'basic knowledge';
|
|
187161
|
+
if (i >= 80) {
|
|
187162
|
+
level = 'Expert';
|
|
187163
|
+
description = 'recognized authority';
|
|
187164
|
+
}
|
|
187165
|
+
else if (i >= 60) {
|
|
187166
|
+
level = 'Advanced';
|
|
187167
|
+
description = 'applied theory';
|
|
187168
|
+
}
|
|
187169
|
+
else if (i >= 40) {
|
|
187170
|
+
level = 'Intermediate';
|
|
187171
|
+
description = 'practical application';
|
|
187172
|
+
}
|
|
187173
|
+
else if (i >= 20) {
|
|
187174
|
+
level = 'Novice';
|
|
187175
|
+
description = 'limited experience';
|
|
187176
|
+
}
|
|
187177
|
+
data.push({ percentile: i, value, level, description });
|
|
187178
|
+
}
|
|
187179
|
+
return data;
|
|
187180
|
+
};
|
|
187181
|
+
/** @internal Exported for testing. */
|
|
187182
|
+
const CustomTooltip = ({ active, payload, label }) => {
|
|
187183
|
+
if (active && payload && payload.length) {
|
|
187184
|
+
return (jsxs("div", { className: "rounded-md border border-gray-200 bg-white p-3 shadow-sm", children: [jsx("p", { className: "text-sm font-medium", children: `Level: ${payload[0].payload.level}` }), jsx("p", { className: "text-xs text-gray-600", children: `Skill Points: ${Math.round(payload[0].value)}` }), jsx("p", { className: "text-xs text-gray-600", children: `Percentile: ${label}%` })] }));
|
|
187185
|
+
}
|
|
187186
|
+
return null;
|
|
187187
|
+
};
|
|
187188
|
+
/**
|
|
187189
|
+
* Presentational skill-leaderboard curve chart. Takes the user's raw
|
|
187190
|
+
* skill points as a prop and derives their percentile + level locally.
|
|
187191
|
+
* Ported verbatim from the skillsai SPA (components/skill-leaderboard-chart.tsx).
|
|
187192
|
+
*/
|
|
187193
|
+
const SkillLeaderboardChart = ({ userSkillPoints = 0, }) => {
|
|
187194
|
+
const [data, setData] = React.useState([]);
|
|
187195
|
+
const [userPosition, setUserPosition] = React.useState(0);
|
|
187196
|
+
React.useEffect(() => {
|
|
187197
|
+
const skillPercentage = userSkillPoints / 500;
|
|
187198
|
+
if (Number.isNaN(skillPercentage)) {
|
|
187199
|
+
setUserPosition(0);
|
|
187200
|
+
}
|
|
187201
|
+
else if (skillPercentage > 1) {
|
|
187202
|
+
setUserPosition(100);
|
|
187203
|
+
}
|
|
187204
|
+
else {
|
|
187205
|
+
setUserPosition(skillPercentage * 100);
|
|
187206
|
+
}
|
|
187207
|
+
}, [userSkillPoints]);
|
|
187208
|
+
React.useEffect(() => {
|
|
187209
|
+
setData(generateCurveData());
|
|
187210
|
+
}, []);
|
|
187211
|
+
const getUserLevel = () => {
|
|
187212
|
+
if (userPosition >= 80)
|
|
187213
|
+
return 'Expert';
|
|
187214
|
+
if (userPosition >= 60)
|
|
187215
|
+
return 'Advanced';
|
|
187216
|
+
if (userPosition >= 40)
|
|
187217
|
+
return 'Intermediate';
|
|
187218
|
+
if (userPosition >= 20)
|
|
187219
|
+
return 'Novice';
|
|
187220
|
+
return 'Beginner';
|
|
187221
|
+
};
|
|
187222
|
+
const getUserPoints = () => {
|
|
187223
|
+
const userDataPoint = data.find((point) => point.percentile === userPosition) ||
|
|
187224
|
+
data.find((point) => point.percentile > userPosition) || {
|
|
187225
|
+
value: 0};
|
|
187226
|
+
return Math.round(userDataPoint.value);
|
|
187227
|
+
};
|
|
187228
|
+
return (jsxs(Fragment$1, { children: [jsx("div", { className: "mb-2 flex justify-between px-2", children: skillLevels.map((level) => (jsx("div", { className: "text-center", children: jsx("p", { className: "text-xs font-medium text-gray-700", children: level.name }) }, level.name))) }), jsx("div", { className: "h-[250px] w-full overflow-x-auto sm:h-[300px]", children: jsx("div", { className: "h-full min-w-[600px]", children: jsx(ResponsiveContainer, { width: "100%", height: "100%", children: jsxs(LineChart, { data: data, margin: { top: 20, right: 20, left: 20, bottom: 60 }, children: [jsx(CartesianGrid, { strokeDasharray: "3 3", vertical: false }), jsx(XAxis, { dataKey: "percentile", type: "number", domain: [0, 100], tickFormatter: (value) => `${value}%`, label: {
|
|
187229
|
+
value: 'Global Learner Percentile',
|
|
187230
|
+
position: 'bottom',
|
|
187231
|
+
offset: 0,
|
|
187232
|
+
dy: 30,
|
|
187233
|
+
} }), jsx(YAxis, { domain: [0, 500], label: {
|
|
187234
|
+
value: 'Skill Points',
|
|
187235
|
+
angle: -90,
|
|
187236
|
+
position: 'left',
|
|
187237
|
+
dx: -30,
|
|
187238
|
+
} }), jsx(Tooltip, { content: jsx(CustomTooltip, {}) }), jsx(ReferenceArea, { x1: 80, x2: 100, fill: "rgba(248, 180, 58, 0.15)", fillOpacity: 0.3 }), skillLevels.map((level, index) => (jsx(ReferenceLine, { x: index * 20, stroke: "rgba(0,0,0,0.1)", strokeDasharray: "3 3", label: {
|
|
187239
|
+
value: level.description,
|
|
187240
|
+
position: 'bottom',
|
|
187241
|
+
fill: '#6B7280',
|
|
187242
|
+
fontSize: 10,
|
|
187243
|
+
dy: 15,
|
|
187244
|
+
} }, level.name))), jsx(ReferenceLine, { x: userPosition, stroke: "#F8B43A", strokeWidth: 2, strokeDasharray: "3 3" }), jsx(Line, { type: "monotone", dataKey: "value", stroke: "#F8B43A", strokeWidth: 3, dot: false, activeDot: { r: 8, fill: '#F8B43A' } })] }) }) }) }), jsxs("div", { className: "mt-4 flex items-center justify-between", children: [jsxs("div", { children: [jsxs("p", { className: "text-sm text-gray-600", children: ["Your Level: ", jsx("span", { className: "font-medium text-amber-500", children: getUserLevel() })] }), jsxs("p", { className: "text-sm text-gray-600", children: ["Skill Points: ", jsx("span", { className: "font-medium", children: getUserPoints() })] })] }), jsxs("div", { className: "text-xs text-gray-500", children: ["Percentile: ", userPosition, "%"] })] })] }));
|
|
187245
|
+
};
|
|
187246
|
+
|
|
187247
|
+
/**
|
|
187248
|
+
* Skeleton row for the AddSkillDialog search results list. Ported from
|
|
187249
|
+
* the skillsai SPA (components/skeleton-add-skills-loading.tsx).
|
|
187250
|
+
*/
|
|
187251
|
+
const SkeletonAddSkillsLoading = () => (jsxs("div", { className: "flex items-center rounded-md border border-gray-200 p-3", children: [jsx("div", { className: "flex-1", children: jsx("div", { className: "flex items-center", children: jsx("div", { className: "h-4 w-24 animate-pulse rounded bg-gray-200" }) }) }), jsx("div", { className: "h-6 w-6 animate-pulse rounded-full bg-gray-200" })] }));
|
|
187252
|
+
|
|
187253
|
+
const DIALOG_TITLES = {
|
|
187254
|
+
earned: 'Add Earned Skill',
|
|
187255
|
+
desired: 'Add Desired Skill',
|
|
187256
|
+
'self-reported': 'Add Self-Reported Skill',
|
|
187257
|
+
};
|
|
187258
|
+
/**
|
|
187259
|
+
* Presentational "add skill" dialog with a debounced search input and
|
|
187260
|
+
* single-select list of skill candidates.
|
|
187261
|
+
*
|
|
187262
|
+
* Ported from the skillsai SPA (components/add-skill-dialog.tsx),
|
|
187263
|
+
* refactored to take data and handlers via props. Callers are expected
|
|
187264
|
+
* to wire the `onSearch` → `useProfileSkills().handleFetchAllSkills`
|
|
187265
|
+
* link and the `onAddSkill` → `handleSkillsUpdate` link themselves.
|
|
187266
|
+
*/
|
|
187267
|
+
const AddSkillDialog = ({ open, onOpenChange, type, fetchedSkills, isFetchingSkills, isFetchingSkillsError, updatingSkill, onSearch, onAddSkill, searchDebounceMs = 500, }) => {
|
|
187268
|
+
const [searchQuery, setSearchQuery] = React.useState('');
|
|
187269
|
+
const [selectedSkill, setSelectedSkill] = React.useState(null);
|
|
187270
|
+
const searchInputRef = React.useRef(null);
|
|
187271
|
+
const debounceTimer = React.useRef(null);
|
|
187272
|
+
// Debounced search: reset timer on every keystroke.
|
|
187273
|
+
React.useEffect(() => {
|
|
187274
|
+
if (debounceTimer.current)
|
|
187275
|
+
clearTimeout(debounceTimer.current);
|
|
187276
|
+
debounceTimer.current = setTimeout(() => {
|
|
187277
|
+
onSearch(searchQuery);
|
|
187278
|
+
}, searchDebounceMs);
|
|
187279
|
+
return () => {
|
|
187280
|
+
if (debounceTimer.current)
|
|
187281
|
+
clearTimeout(debounceTimer.current);
|
|
187282
|
+
};
|
|
187283
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
187284
|
+
}, [searchQuery]);
|
|
187285
|
+
// Focus the search input when the dialog opens.
|
|
187286
|
+
React.useEffect(() => {
|
|
187287
|
+
if (open && searchInputRef.current) {
|
|
187288
|
+
const t = setTimeout(() => { var _a; return (_a = searchInputRef.current) === null || _a === void 0 ? void 0 : _a.focus(); }, 100);
|
|
187289
|
+
return () => clearTimeout(t);
|
|
187290
|
+
}
|
|
187291
|
+
}, [open]);
|
|
187292
|
+
// Reset local state when the dialog closes.
|
|
187293
|
+
React.useEffect(() => {
|
|
187294
|
+
if (!open) {
|
|
187295
|
+
setSearchQuery('');
|
|
187296
|
+
setSelectedSkill(null);
|
|
187297
|
+
}
|
|
187298
|
+
}, [open]);
|
|
187299
|
+
const handleAddSkill = () => {
|
|
187300
|
+
if (updatingSkill || !selectedSkill)
|
|
187301
|
+
return;
|
|
187302
|
+
onAddSkill(selectedSkill);
|
|
187303
|
+
};
|
|
187304
|
+
return (jsx(Dialog$1, { open: open, onOpenChange: onOpenChange, children: jsxs(DialogContent$1, { className: "overflow-hidden p-0 sm:max-w-md", children: [jsx(DialogHeader, { className: "border-b px-6 py-4", children: jsx("div", { className: "flex items-center justify-between", children: jsx(DialogTitle$1, { className: "text-lg font-medium text-gray-600", children: DIALOG_TITLES[type] }) }) }), jsxs("div", { className: "p-6", children: [jsxs("div", { className: "relative mb-4", children: [jsx("div", { className: "pointer-events-none absolute inset-y-0 left-3 flex items-center", children: jsx(Search, { className: "h-4 w-4 text-gray-400" }) }), jsx("input", { ref: searchInputRef, type: "text", placeholder: "Search skills...", value: searchQuery, onChange: (e) => setSearchQuery(e.target.value), className: "w-full rounded-md border border-gray-200 bg-gray-50 py-2 pr-4 pl-10 text-sm text-gray-700 focus:ring-1 focus:ring-amber-500 focus:outline-none" })] }), ((!isFetchingSkills && isFetchingSkillsError) ||
|
|
187305
|
+
(!isFetchingSkills && fetchedSkills.length === 0)) && (jsx(DefaultEmptyBox, { message: "No skills found" })), jsxs("div", { className: "max-h-[300px] space-y-2 overflow-y-auto pr-1", children: [isFetchingSkills && (jsx(SkeletonMultiplier, { multiplier: 6, Skeleton: SkeletonAddSkillsLoading })), !isFetchingSkills &&
|
|
187306
|
+
!isFetchingSkillsError &&
|
|
187307
|
+
fetchedSkills.map((row, index) => {
|
|
187308
|
+
const skill = row === null || row === void 0 ? void 0 : row.data;
|
|
187309
|
+
const isSelected = !!(skill === null || skill === void 0 ? void 0 : skill.skill_id) && (selectedSkill === null || selectedSkill === void 0 ? void 0 : selectedSkill.skill_id) === skill.skill_id;
|
|
187310
|
+
return (jsxs("div", { onClick: () => skill && setSelectedSkill(skill), className: `flex cursor-pointer items-center rounded-md border p-3 transition-colors ${isSelected
|
|
187311
|
+
? 'border-amber-500 bg-amber-50'
|
|
187312
|
+
: 'border-gray-200 hover:bg-gray-50'}`, children: [jsx("div", { className: "flex-1", children: jsx("div", { className: "flex items-center", children: jsx("span", { className: "text-sm font-medium text-gray-700", children: skill === null || skill === void 0 ? void 0 : skill.name }) }) }), jsx("div", { className: `flex h-6 w-6 items-center justify-center rounded-full ${isSelected ? 'bg-amber-500 text-white' : 'bg-gray-100 text-gray-400'}`, children: isSelected ? jsx(Check, { className: "h-4 w-4" }) : jsx(Plus, { className: "h-4 w-4" }) })] }, `skill-${index}`));
|
|
187313
|
+
})] })] }), jsx("div", { className: "flex justify-end border-t p-4", children: jsx("button", { onClick: handleAddSkill, disabled: !selectedSkill, className: `rounded-md px-4 py-2 font-medium text-white transition-colors ${selectedSkill
|
|
187314
|
+
? 'bg-gradient-to-r from-[var(--button-primary-gradient-from)] to-[var(--button-primary-gradient-to)] hover:opacity-[var(--button-primary-hover-opacity)]'
|
|
187315
|
+
: 'cursor-not-allowed bg-gray-300'}`, children: updatingSkill ? 'Submitting...' : 'Add Skill' }) })] }) }));
|
|
187316
|
+
};
|
|
187317
|
+
|
|
187318
|
+
/**
|
|
187319
|
+
* Skeleton row for the CreatePathwayModal content search list. Ported
|
|
187320
|
+
* from the skillsai SPA (components/skeleton-create-pathway-search-list.tsx).
|
|
187321
|
+
*/
|
|
187322
|
+
const SkeletonCreatePathwaySearchList = () => (jsx("div", { className: "animate-pulse overflow-hidden rounded-lg border border-gray-200", children: jsxs("div", { className: "flex items-center bg-white p-3", children: [jsx("div", { className: "relative mr-3 h-12 w-12 flex-shrink-0 overflow-hidden rounded-md bg-gray-200", children: jsx("div", { className: "h-full w-full animate-pulse bg-gray-300" }) }), jsxs("div", { className: "flex-1", children: [jsx("div", { className: "mb-2 h-4 animate-pulse rounded bg-gray-200" }), jsx("div", { className: "h-3 w-3/4 animate-pulse rounded bg-gray-100" })] }), jsx("div", { className: "h-6 w-6 animate-pulse rounded-full bg-gray-200" })] }) }));
|
|
187323
|
+
|
|
187324
|
+
/**
|
|
187325
|
+
* Skeleton row for the EducationBox / ExperienceBox lists. Ported from
|
|
187326
|
+
* the skillsai SPA (components/profile/skeleton-education-box.tsx).
|
|
187327
|
+
*/
|
|
187328
|
+
const SkeletonEducationBox = () => (jsxs("div", { children: [jsxs("div", { className: "flex items-center", children: [jsx(Skeleton, { className: "h-5 w-24" }), jsx(Skeleton, { className: "ml-2 h-4 w-4" })] }), jsx("div", { className: "mt-1", children: jsx(Skeleton, { className: "h-4 w-48" }) }), jsx("div", { className: "mt-2", children: jsx(Skeleton, { className: "h-4 w-32" }) })] }));
|
|
187329
|
+
|
|
187330
|
+
const FALLBACK_CREDENTIAL_IMG = 'data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48"><circle cx="24" cy="24" r="22" fill="%23e5e7eb"/></svg>';
|
|
187331
|
+
const CredentialMiniBox = ({ credential, onClick = () => { }, minified = false, iconSize = 48, defaultImage = FALLBACK_CREDENTIAL_IMG, }) => {
|
|
187332
|
+
var _a, _b, _c, _d;
|
|
187333
|
+
return (jsxs("div", { className: "flex cursor-pointer items-start rounded-lg border border-gray-200 bg-white p-4 transition-shadow hover:shadow-md", onClick: onClick, children: [jsx("div", { className: "mr-4 h-12 w-12 flex-shrink-0 overflow-hidden rounded-full", children: jsx(Image$4, { src: ((_a = credential.credentialDetails) === null || _a === void 0 ? void 0 : _a.iconImage) || defaultImage, alt: ((_b = credential.credentialDetails) === null || _b === void 0 ? void 0 : _b.name) || 'Credential', width: iconSize, height: iconSize, className: "h-full w-full object-cover" }) }), jsxs("div", { children: [jsx("h3", { className: "text-sm font-medium text-gray-800", children: (_c = credential.credentialDetails) === null || _c === void 0 ? void 0 : _c.name }), !minified && jsx("p", { className: "text-xs text-gray-600", children: (_d = credential.course) === null || _d === void 0 ? void 0 : _d.name }), jsxs("p", { className: "mt-1 text-xs text-gray-500", children: ["Earned on: ", credential.issuedOn ? dayjs(credential.issuedOn).format('MMM D, YYYY') : '-'] })] })] }));
|
|
187334
|
+
};
|
|
187335
|
+
|
|
187336
|
+
/**
|
|
187337
|
+
* Presentational credentials grid for the public profile page. Caller
|
|
187338
|
+
* supplies the credentials list + loading flags (typically from the
|
|
187339
|
+
* useProfileCredentials hook).
|
|
187340
|
+
*
|
|
187341
|
+
* Ported from the skillsai SPA (components/profile/credential-box.tsx).
|
|
187342
|
+
*/
|
|
187343
|
+
const CredentialBox = ({ credentials, isLoading = false, isError = false, }) => {
|
|
187344
|
+
const isEmpty = !isLoading && !isError && credentials.length === 0;
|
|
187345
|
+
return (jsxs("div", { className: "rounded-lg border border-gray-200 bg-white p-6", children: [jsx("h2", { className: "mb-4 text-lg font-medium text-gray-800", children: "Credentials" }), (isError || isEmpty) && (jsx(DefaultEmptyBox, { message: "No credentials found.", className: "w-full" })), jsxs("div", { className: "grid grid-cols-1 gap-4 md:grid-cols-3", children: [isLoading && jsx(SkeletonMultiplier, { Skeleton: CredentialMiniBoxSkeleton, multiplier: 8 }), !isLoading &&
|
|
187346
|
+
!isError &&
|
|
187347
|
+
credentials.length > 0 &&
|
|
187348
|
+
credentials.map((credential) => (jsx(CredentialMiniBox, { credential: credential }, credential.entityId)))] })] }));
|
|
187349
|
+
};
|
|
187350
|
+
|
|
187351
|
+
const formatYearRange$1 = (start, end) => {
|
|
187352
|
+
if (!start && !end)
|
|
187353
|
+
return '';
|
|
187354
|
+
const startYear = start ? dayjs(start).format('YYYY') : '';
|
|
187355
|
+
const endYear = end ? dayjs(end).format('YYYY') : 'Present';
|
|
187356
|
+
if (start && end && startYear === endYear)
|
|
187357
|
+
return endYear;
|
|
187358
|
+
return `${startYear} - ${endYear}`;
|
|
187359
|
+
};
|
|
187360
|
+
/**
|
|
187361
|
+
* Presentational education list for the public profile page. The
|
|
187362
|
+
* skillsai SPA also rendered an inline EducationDialog and
|
|
187363
|
+
* AddInstitutionDialog — both are caller-managed now (the caller can
|
|
187364
|
+
* open them in response to onEditEducation).
|
|
187365
|
+
*
|
|
187366
|
+
* Ported from the skillsai SPA (components/profile/education-box.tsx).
|
|
187367
|
+
*/
|
|
187368
|
+
const EducationBox = ({ education = [], isLoading = false, isError = false, onEditEducation, }) => {
|
|
187369
|
+
const isEmpty = !isLoading && !isError && education.length === 0;
|
|
187370
|
+
return (jsxs("div", { className: "rounded-lg border border-gray-200 bg-white p-6", children: [jsx("h2", { className: "mb-4 text-lg font-medium text-gray-800", children: "Education" }), jsxs("div", { className: "space-y-8", children: [isLoading && jsx(SkeletonMultiplier, { multiplier: 4, Skeleton: SkeletonEducationBox }), (isError || isEmpty) && jsx(DefaultEmptyBox, { message: "No education found." }), !isLoading &&
|
|
187371
|
+
!isError &&
|
|
187372
|
+
education.map((entry, index) => {
|
|
187373
|
+
var _a;
|
|
187374
|
+
return (jsxs("div", { children: [jsxs("div", { className: "flex items-center", children: [jsx("h3", { className: "text-base font-medium text-amber-500", children: entry.degree }), onEditEducation && (jsx("button", { onClick: () => onEditEducation(entry), "aria-label": "Edit education entry", className: "ml-2 text-amber-500 transition-colors hover:text-amber-600", children: jsx(Pen, { className: "h-4 w-4" }) }))] }), jsxs("div", { className: "mt-1 text-gray-700", children: [(_a = entry === null || entry === void 0 ? void 0 : entry.institution) === null || _a === void 0 ? void 0 : _a.name, " | ", formatYearRange$1(entry.start_date, entry.end_date), entry.grade ? ` | Grade: ${entry.grade}` : ''] }), entry.description && jsx("p", { className: "mt-2 text-gray-600", children: entry.description })] }, index));
|
|
187375
|
+
})] })] }));
|
|
187376
|
+
};
|
|
187377
|
+
|
|
187378
|
+
const formatYearRange = (start, end) => {
|
|
187379
|
+
if (!start && !end)
|
|
187380
|
+
return '';
|
|
187381
|
+
const startYear = start ? dayjs(start).format('YYYY') : '';
|
|
187382
|
+
const endYear = end ? dayjs(end).format('YYYY') : 'Present';
|
|
187383
|
+
if (start && end && startYear === endYear)
|
|
187384
|
+
return endYear;
|
|
187385
|
+
return `${startYear} - ${endYear}`;
|
|
187386
|
+
};
|
|
187387
|
+
/**
|
|
187388
|
+
* Presentational work-experience list for the public profile page.
|
|
187389
|
+
*
|
|
187390
|
+
* Ported from the skillsai SPA (components/profile/experience-box.tsx).
|
|
187391
|
+
*/
|
|
187392
|
+
const ExperienceBox = ({ experience = [], isLoading = false, isError = false, onEditExperience, }) => {
|
|
187393
|
+
const isEmpty = !isLoading && !isError && experience.length === 0;
|
|
187394
|
+
return (jsxs("div", { className: "rounded-lg border border-gray-200 bg-white p-6", children: [jsx("h2", { className: "mb-4 text-lg font-medium text-gray-800", children: "Work Experience" }), jsxs("div", { className: "space-y-8", children: [isLoading && jsx(SkeletonMultiplier, { multiplier: 4, Skeleton: SkeletonEducationBox }), (isError || isEmpty) && jsx(DefaultEmptyBox, { message: "No experience found." }), !isLoading &&
|
|
187395
|
+
!isError &&
|
|
187396
|
+
experience.map((entry, index) => {
|
|
187397
|
+
var _a;
|
|
187398
|
+
return (jsxs("div", { children: [jsxs("div", { className: "flex items-center", children: [jsx("h3", { className: "text-base font-medium text-amber-500", children: entry.title }), onEditExperience && (jsx("button", { onClick: () => onEditExperience(entry), "aria-label": "Edit experience entry", className: "ml-2 text-amber-500 transition-colors hover:text-amber-600", children: jsx(Pen, { className: "h-4 w-4" }) }))] }), jsxs("div", { className: "mt-1 text-gray-700", children: [(_a = entry.company) === null || _a === void 0 ? void 0 : _a.name, " | ", formatYearRange(entry.start_date, entry.end_date)] }), entry.description && jsx("p", { className: "mt-2 text-gray-600", children: entry.description })] }, index));
|
|
187399
|
+
})] })] }));
|
|
187400
|
+
};
|
|
187401
|
+
|
|
187402
|
+
const isEmpty$1 = (xs) => !xs || xs.length === 0;
|
|
187403
|
+
/**
|
|
187404
|
+
* Presentational skills list for the public profile page. Splits the
|
|
187405
|
+
* three skill buckets (earned, self-reported, desired) into their own
|
|
187406
|
+
* sections, each with its own loading skeleton and empty state.
|
|
187407
|
+
*
|
|
187408
|
+
* Ported from the skillsai SPA (components/profile/skills-box.tsx).
|
|
187409
|
+
*/
|
|
187410
|
+
const SkillsBox = ({ earnedSkills, earnedSkillsLoading = false, earnedSkillsError = false, selfReportedSkills, selfReportedSkillsLoading = false, selfReportedSkillsError = false, desiredSkills, desiredSkillsLoading = false, desiredSkillsError = false, }) => {
|
|
187411
|
+
var _a, _b, _c;
|
|
187412
|
+
return (jsxs("div", { className: "rounded-lg border border-gray-200 bg-white p-6", children: [jsx("h2", { className: "mb-4 text-lg font-medium text-gray-800", children: "Skills" }), jsxs("div", { className: "mb-6", children: [jsx("h3", { className: "mb-4 text-base font-medium text-gray-700", children: "Earned" }), !earnedSkillsLoading && (earnedSkillsError || isEmpty$1(earnedSkills === null || earnedSkills === void 0 ? void 0 : earnedSkills.resources)) && (jsx(DefaultEmptyBox, { className: "w-full", message: "You don't have any earned skills yet." })), jsxs("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4", children: [earnedSkillsLoading && jsx(SkeletonMultiplier, { Skeleton: SkeletonSkillBox, multiplier: 6 }), !earnedSkillsLoading &&
|
|
187413
|
+
!earnedSkillsError &&
|
|
187414
|
+
((_a = earnedSkills === null || earnedSkills === void 0 ? void 0 : earnedSkills.resources) === null || _a === void 0 ? void 0 : _a.map((skill, index) => (jsx(SkillBox, { onSkillClick: () => { }, skill: {
|
|
187415
|
+
name: skill.name || '',
|
|
187416
|
+
level: skill.points || 0,
|
|
187417
|
+
starred: false,
|
|
187418
|
+
} }, index))))] })] }), jsxs("div", { className: "mb-6", children: [jsx("h3", { className: "mb-4 text-base font-medium text-gray-700", children: "Self-Reported" }), !selfReportedSkillsLoading &&
|
|
187419
|
+
(selfReportedSkillsError || isEmpty$1(selfReportedSkills === null || selfReportedSkills === void 0 ? void 0 : selfReportedSkills.skills)) && (jsx(DefaultEmptyBox, { className: "w-full", message: "You don't have any self-reported skills yet." })), jsxs("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4", children: [selfReportedSkillsLoading && (jsx(SkeletonMultiplier, { Skeleton: SkeletonSkillBox, multiplier: 6 })), !selfReportedSkillsLoading &&
|
|
187420
|
+
!selfReportedSkillsError &&
|
|
187421
|
+
((_b = selfReportedSkills === null || selfReportedSkills === void 0 ? void 0 : selfReportedSkills.skills) === null || _b === void 0 ? void 0 : _b.map((skill, index) => {
|
|
187422
|
+
var _a, _b;
|
|
187423
|
+
return (jsx(SkillBox, { skill: {
|
|
187424
|
+
name: skill.name || '',
|
|
187425
|
+
level: ((_b = (_a = selfReportedSkills === null || selfReportedSkills === void 0 ? void 0 : selfReportedSkills.data) === null || _a === void 0 ? void 0 : _a.level) === null || _b === void 0 ? void 0 : _b[index]) || 0,
|
|
187426
|
+
starred: true,
|
|
187427
|
+
} }, index));
|
|
187428
|
+
}))] })] }), jsxs("div", { children: [jsx("h3", { className: "mb-4 text-base font-medium text-gray-700", children: "Desired" }), !desiredSkillsLoading && (desiredSkillsError || isEmpty$1(desiredSkills === null || desiredSkills === void 0 ? void 0 : desiredSkills.skills)) && (jsx(DefaultEmptyBox, { className: "w-full", message: "You don't have any desired skills yet." })), jsxs("div", { className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4", children: [desiredSkillsLoading && (jsx(SkeletonMultiplier, { Skeleton: SkeletonSkillBox, multiplier: 6 })), !desiredSkillsLoading &&
|
|
187429
|
+
!desiredSkillsError &&
|
|
187430
|
+
((_c = desiredSkills === null || desiredSkills === void 0 ? void 0 : desiredSkills.skills) === null || _c === void 0 ? void 0 : _c.map((skill, index) => {
|
|
187431
|
+
var _a, _b;
|
|
187432
|
+
return (jsx(SkillBox, { showRating: false, skill: {
|
|
187433
|
+
name: skill.name || '',
|
|
187434
|
+
level: ((_b = (_a = desiredSkills === null || desiredSkills === void 0 ? void 0 : desiredSkills.data) === null || _a === void 0 ? void 0 : _a.level) === null || _b === void 0 ? void 0 : _b[index]) || 0,
|
|
187435
|
+
starred: true,
|
|
187436
|
+
} }, index));
|
|
187437
|
+
}))] })] })] }));
|
|
187438
|
+
};
|
|
187439
|
+
|
|
187440
|
+
try {
|
|
187441
|
+
__webpack_exports__GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${__webpack_exports__version}/build/pdf.worker.min.mjs`;
|
|
187442
|
+
}
|
|
187443
|
+
catch (_a) {
|
|
187444
|
+
// pdfjs may not be configurable in non-browser environments; ignore silently.
|
|
187445
|
+
}
|
|
187446
|
+
const ResumeBoxSkeleton = () => (jsx("div", { className: "h-[200px] w-full animate-pulse rounded-lg bg-gray-200" }));
|
|
187447
|
+
/**
|
|
187448
|
+
* Presentational PDF resume viewer with paging, zoom, and rotation
|
|
187449
|
+
* controls. Uses react-pdf, so it must run client-side.
|
|
187450
|
+
*
|
|
187451
|
+
* Ported from the skillsai SPA (components/profile/resume-box.tsx).
|
|
187452
|
+
*/
|
|
187453
|
+
const ResumeBox = ({ resumeUrl, isLoading = false, isError = false, }) => {
|
|
187454
|
+
const [resumeLoadError, setResumeLoadError] = React.useState(false);
|
|
187455
|
+
const [numPages, setNumPages] = React.useState(null);
|
|
187456
|
+
const [pageNumber, setPageNumber] = React.useState(1);
|
|
187457
|
+
const [scale, setScale] = React.useState(1.0);
|
|
187458
|
+
const [rotation, setRotation] = React.useState(0);
|
|
187459
|
+
const onDocumentLoadSuccess = ({ numPages }) => {
|
|
187460
|
+
setNumPages(numPages);
|
|
187461
|
+
};
|
|
187462
|
+
const onDocumentLoadError = () => setResumeLoadError(true);
|
|
187463
|
+
const goToPrevPage = () => setPageNumber((p) => p - 1);
|
|
187464
|
+
const goToNextPage = () => setPageNumber((p) => p + 1);
|
|
187465
|
+
const zoomIn = () => setScale((s) => s + 0.2);
|
|
187466
|
+
const zoomOut = () => setScale((s) => (s > 0.4 ? s - 0.2 : s));
|
|
187467
|
+
const rotateLeft = () => setRotation((r) => r - 90);
|
|
187468
|
+
const rotateRight = () => setRotation((r) => r + 90);
|
|
187469
|
+
return (jsxs("div", { className: "rounded-lg border border-gray-200 bg-white p-6", children: [jsx("h2", { className: "mb-4 text-lg font-medium text-gray-800", children: "Resume" }), isLoading && jsx(ResumeBoxSkeleton, {}), ((!isLoading && isError) || (!isLoading && !isError && !resumeUrl)) && (jsx(DefaultEmptyBox, { message: "No resume found.", className: "w-full" })), !isLoading && !isError && resumeUrl && !resumeLoadError && (jsxs(Fragment$1, { children: [jsxs("div", { className: "mb-5 flex flex-col items-center justify-center rounded-lg bg-gray-100 p-2.5 md:flex-row", children: [jsxs("div", { className: "mb-2.5 flex items-center md:mr-5 md:mb-0", children: [jsx("button", { onClick: goToPrevPage, disabled: pageNumber <= 1, className: "mx-1 rounded border-none bg-amber-500 px-3 py-2 text-white transition-colors hover:bg-amber-400 disabled:cursor-not-allowed disabled:bg-gray-300", children: "Previous Page" }), jsxs("span", { className: "mx-2.5 font-bold", children: ["Page ", pageNumber, " of ", numPages] }), jsx("button", { onClick: goToNextPage, disabled: pageNumber >= (numPages !== null && numPages !== void 0 ? numPages : 0), className: "mx-1 rounded border-none bg-amber-500 px-3 py-2 text-white transition-colors hover:bg-amber-400 disabled:cursor-not-allowed disabled:bg-gray-300", children: "Next Page" })] }), jsxs("div", { className: "mb-2.5 flex items-center md:mr-5 md:mb-0", children: [jsx("button", { onClick: zoomOut, disabled: scale <= 0.4, "aria-label": "Zoom out", className: "mx-1 rounded border-none bg-amber-500 px-3 py-2 text-white transition-colors hover:bg-amber-400 disabled:cursor-not-allowed disabled:bg-gray-300", children: "-" }), jsxs("span", { className: "mx-2.5 font-bold", children: ["Zoom: ", Math.round(scale * 100), "%"] }), jsx("button", { onClick: zoomIn, "aria-label": "Zoom in", className: "mx-1 rounded border-none bg-amber-500 px-3 py-2 text-white transition-colors hover:bg-amber-400 disabled:cursor-not-allowed disabled:bg-gray-300", children: "+" })] }), jsxs("div", { className: "flex items-center", children: [jsx("button", { onClick: rotateLeft, className: "mx-1 rounded border-none bg-amber-500 px-3 py-2 text-white transition-colors hover:bg-amber-400", children: "Rotate Left" }), jsx("button", { onClick: rotateRight, className: "mx-1 rounded border-none bg-amber-500 px-3 py-2 text-white transition-colors hover:bg-amber-400", children: "Rotate Right" })] })] }), jsx("div", { className: "flex min-h-[80vh] items-center justify-center", children: jsx(Document$1, { file: resumeUrl, onLoadSuccess: onDocumentLoadSuccess, onLoadError: onDocumentLoadError, loading: jsx(ResumeBoxSkeleton, {}), children: jsx(Page, { pageNumber: pageNumber, scale: scale, rotate: rotation }) }) })] })), resumeLoadError && jsx(DefaultEmptyBox, { message: "Error loading resume.", className: "w-full" })] }));
|
|
187470
|
+
};
|
|
187471
|
+
|
|
187472
|
+
/**
|
|
187473
|
+
* Wrapper around the course-metadata RTK endpoints that exposes
|
|
187474
|
+
* promise-returning fetch helpers for use inside effects/callbacks.
|
|
187475
|
+
*
|
|
187476
|
+
* Ported from the skillsai SPA (hooks/courses/use-course-metadata.ts).
|
|
187477
|
+
*/
|
|
187478
|
+
const useCourseMetadata = () => {
|
|
187479
|
+
const [getCourseMetaData, { isLoading, isError }] = useLazyGetCourseMetaDataQuery();
|
|
187480
|
+
const [getCourseCompletionOutlines, { isLoading: isLoadingCompletionOutlines, isError: isErrorCompletionOutlines },] = useLazyGetCourseCompletionOutlinesQuery();
|
|
187481
|
+
const [getCourseEligibility] = useLazyGetCourseEligibilityQuery();
|
|
187482
|
+
const handleFetchCourseMetaData = async (courseKey) => {
|
|
187483
|
+
try {
|
|
187484
|
+
const { data: courseMetaData } = await getCourseMetaData({ courseKey }, true);
|
|
187485
|
+
return courseMetaData;
|
|
187486
|
+
}
|
|
187487
|
+
catch (_a) {
|
|
187488
|
+
return undefined;
|
|
187489
|
+
}
|
|
187490
|
+
};
|
|
187491
|
+
const handleFetchCourseEligibility = async (courseKey) => {
|
|
187492
|
+
try {
|
|
187493
|
+
const { data: courseEligibility } = await getCourseEligibility({ courseKey });
|
|
187494
|
+
return courseEligibility || null;
|
|
187495
|
+
}
|
|
187496
|
+
catch (_a) {
|
|
187497
|
+
return null;
|
|
187498
|
+
}
|
|
187499
|
+
};
|
|
187500
|
+
const handleFetchCourseCompletionOutlines = async (courseKey) => {
|
|
187501
|
+
try {
|
|
187502
|
+
const { data: courseCompletionOutlines } = await getCourseCompletionOutlines({ courseKey });
|
|
187503
|
+
return courseCompletionOutlines;
|
|
187504
|
+
}
|
|
187505
|
+
catch (_a) {
|
|
187506
|
+
return undefined;
|
|
187507
|
+
}
|
|
187508
|
+
};
|
|
187509
|
+
return {
|
|
187510
|
+
handleFetchCourseMetaData,
|
|
187511
|
+
handleFetchCourseCompletionOutlines,
|
|
187512
|
+
handleFetchCourseEligibility,
|
|
187513
|
+
isLoading,
|
|
187514
|
+
isError,
|
|
187515
|
+
isLoadingCompletionOutlines,
|
|
187516
|
+
isErrorCompletionOutlines,
|
|
187517
|
+
};
|
|
187518
|
+
};
|
|
187519
|
+
|
|
187520
|
+
/**
|
|
187521
|
+
* Fetches the current user's enrolled or assigned courses (paged), then
|
|
187522
|
+
* hydrates each row with course metadata from the LMS. Search can either
|
|
187523
|
+
* hit the API or filter client-side on the already-fetched page.
|
|
187524
|
+
*
|
|
187525
|
+
* Ported from the skillsai SPA (hooks/courses/use-user-courses.ts).
|
|
187526
|
+
*/
|
|
187527
|
+
const useUserCourses = ({ limit = 8, search = '', page = 1, courseType = 'enrolled', useAPISearch = false, }) => {
|
|
187528
|
+
const { metadata } = useTenantMetadata({ org: getTenant() });
|
|
187529
|
+
const [getUserEnrolledCourses, { isLoading: isLoadingEnrolledCourses, isError: errorEnrolledCourses },] = useLazyGetUserEnrolledCoursesQuery();
|
|
187530
|
+
const [getUserAssignedCourses, { isLoading: isLoadingAssignedCourses, isError: errorAssignedCourses },] = useLazyGetUserAssignedCoursesQuery();
|
|
187531
|
+
const { handleFetchCourseMetaData } = useCourseMetadata();
|
|
187532
|
+
const [userCoursesWithMetaData, setUserCoursesWithMetaData] = useState([]);
|
|
187533
|
+
const [filteredCoursesWithMetaData, setFilteredCoursesWithMetaData] = useState([]);
|
|
187534
|
+
const [isLoadingCoursesMetaData, setIsLoadingCoursesMetaData] = useState(false);
|
|
187535
|
+
const [pagination, setPagination] = useState(null);
|
|
187536
|
+
const handleFetchAllCoursesMetaData = async (enrolledCourses) => {
|
|
187537
|
+
try {
|
|
187538
|
+
const coursesMetaData = await Promise.all(enrolledCourses.map((course) => handleFetchCourseMetaData(course.course_id)));
|
|
187539
|
+
const coursesWithMetaData = enrolledCourses.map((course, index) => ({
|
|
187540
|
+
...course,
|
|
187541
|
+
name: course.course_name,
|
|
187542
|
+
edx_data: coursesMetaData[index],
|
|
187543
|
+
}));
|
|
187544
|
+
setUserCoursesWithMetaData(coursesWithMetaData);
|
|
187545
|
+
setFilteredCoursesWithMetaData(coursesWithMetaData);
|
|
187546
|
+
}
|
|
187547
|
+
catch (_a) {
|
|
187548
|
+
setUserCoursesWithMetaData([]);
|
|
187549
|
+
setFilteredCoursesWithMetaData([]);
|
|
187550
|
+
}
|
|
187551
|
+
finally {
|
|
187552
|
+
setIsLoadingCoursesMetaData(false);
|
|
187553
|
+
}
|
|
187554
|
+
};
|
|
187555
|
+
const handleFetchCourses = async () => {
|
|
187556
|
+
try {
|
|
187557
|
+
setUserCoursesWithMetaData([]);
|
|
187558
|
+
setFilteredCoursesWithMetaData([]);
|
|
187559
|
+
setIsLoadingCoursesMetaData(true);
|
|
187560
|
+
if (courseType === 'assigned') {
|
|
187561
|
+
const userId = getUserId();
|
|
187562
|
+
if (userId == null) {
|
|
187563
|
+
setIsLoadingCoursesMetaData(false);
|
|
187564
|
+
return;
|
|
187565
|
+
}
|
|
187566
|
+
const { data: assignedCourses } = await getUserAssignedCourses({
|
|
187567
|
+
user_id: userId,
|
|
187568
|
+
query: {
|
|
187569
|
+
page_size: limit,
|
|
187570
|
+
...(search && { search }),
|
|
187571
|
+
...(page && { page }),
|
|
187572
|
+
},
|
|
187573
|
+
}, true);
|
|
187574
|
+
if (Array.isArray(assignedCourses === null || assignedCourses === void 0 ? void 0 : assignedCourses.results)) {
|
|
187575
|
+
setPagination({
|
|
187576
|
+
count: (assignedCourses === null || assignedCourses === void 0 ? void 0 : assignedCourses.count) || 0,
|
|
187577
|
+
current_page: page,
|
|
187578
|
+
total_pages: Math.ceil(((assignedCourses === null || assignedCourses === void 0 ? void 0 : assignedCourses.count) || 0) / limit),
|
|
187579
|
+
});
|
|
187580
|
+
await handleFetchAllCoursesMetaData(assignedCourses.results);
|
|
187581
|
+
}
|
|
187582
|
+
else {
|
|
187583
|
+
setIsLoadingCoursesMetaData(false);
|
|
187584
|
+
}
|
|
187585
|
+
}
|
|
187586
|
+
else {
|
|
187587
|
+
const username = getUserName();
|
|
187588
|
+
if (!username) {
|
|
187589
|
+
setIsLoadingCoursesMetaData(false);
|
|
187590
|
+
return;
|
|
187591
|
+
}
|
|
187592
|
+
const { data: enrolledCourses } = await getUserEnrolledCourses({
|
|
187593
|
+
username,
|
|
187594
|
+
query: {
|
|
187595
|
+
page_size: limit,
|
|
187596
|
+
...(search && { search }),
|
|
187597
|
+
...(page && { page }),
|
|
187598
|
+
platform_key: getTenant(),
|
|
187599
|
+
...((metadata === null || metadata === void 0 ? void 0 : metadata.skills_include_community_courses) && { include_default_platform: 1 }),
|
|
187600
|
+
},
|
|
187601
|
+
}, true);
|
|
187602
|
+
if (Array.isArray(enrolledCourses === null || enrolledCourses === void 0 ? void 0 : enrolledCourses.results)) {
|
|
187603
|
+
setPagination({
|
|
187604
|
+
count: (enrolledCourses === null || enrolledCourses === void 0 ? void 0 : enrolledCourses.count) || 0,
|
|
187605
|
+
current_page: page,
|
|
187606
|
+
total_pages: Math.ceil(((enrolledCourses === null || enrolledCourses === void 0 ? void 0 : enrolledCourses.count) || 0) / limit),
|
|
187607
|
+
});
|
|
187608
|
+
await handleFetchAllCoursesMetaData(enrolledCourses.results);
|
|
187609
|
+
}
|
|
187610
|
+
else {
|
|
187611
|
+
setIsLoadingCoursesMetaData(false);
|
|
187612
|
+
}
|
|
187613
|
+
}
|
|
187614
|
+
}
|
|
187615
|
+
catch (_a) {
|
|
187616
|
+
setUserCoursesWithMetaData([]);
|
|
187617
|
+
setFilteredCoursesWithMetaData([]);
|
|
187618
|
+
setIsLoadingCoursesMetaData(false);
|
|
187619
|
+
}
|
|
187620
|
+
};
|
|
187621
|
+
const handleInPageSearch = () => {
|
|
187622
|
+
if (search.length > 2) {
|
|
187623
|
+
const filtered = userCoursesWithMetaData.filter((course) => course.name.toLowerCase().includes(search.toLowerCase()));
|
|
187624
|
+
setFilteredCoursesWithMetaData(filtered);
|
|
187625
|
+
}
|
|
187626
|
+
else {
|
|
187627
|
+
setFilteredCoursesWithMetaData(userCoursesWithMetaData);
|
|
187628
|
+
}
|
|
187629
|
+
};
|
|
187630
|
+
useEffect(() => {
|
|
187631
|
+
if (!useAPISearch) {
|
|
187632
|
+
handleInPageSearch();
|
|
187633
|
+
}
|
|
187634
|
+
else {
|
|
187635
|
+
handleFetchCourses();
|
|
187636
|
+
}
|
|
187637
|
+
}, [search, useAPISearch]);
|
|
187638
|
+
useEffect(() => {
|
|
187639
|
+
handleFetchCourses();
|
|
187640
|
+
}, [
|
|
187641
|
+
courseType,
|
|
187642
|
+
page,
|
|
187643
|
+
metadata === null || metadata === void 0 ? void 0 : metadata.skills_include_community_courses,
|
|
187644
|
+
]);
|
|
187645
|
+
return {
|
|
187646
|
+
userCourses: filteredCoursesWithMetaData,
|
|
187647
|
+
isLoadingUserCourses: isLoadingEnrolledCourses || isLoadingAssignedCourses || isLoadingCoursesMetaData,
|
|
187648
|
+
errorUserCourses: errorEnrolledCourses || errorAssignedCourses,
|
|
187649
|
+
pagination,
|
|
187650
|
+
};
|
|
187651
|
+
};
|
|
187652
|
+
|
|
187653
|
+
/**
|
|
187654
|
+
* Fetches the logged-in user's credential assertions and maintains a
|
|
187655
|
+
* client-side filtered list based on a search string.
|
|
187656
|
+
*
|
|
187657
|
+
* Ported from the skillsai SPA (hooks/profile/use-profile-credentials.ts).
|
|
187658
|
+
*/
|
|
187659
|
+
const useProfileCredentials = ({ search = '', maxCredentials, }) => {
|
|
187660
|
+
const [getUserCredentials, { isLoading, isError }] = useLazyGetUserCredentialsQuery();
|
|
187661
|
+
const [filteredCredentials, setFilteredCredentials] = useState([]);
|
|
187662
|
+
const [fetchedCredentials, setFetchedCredentials] = useState([]);
|
|
187663
|
+
const [credentialsLoading, setCredentialsLoading] = useState(false);
|
|
187664
|
+
const handleFetchCredentials = async () => {
|
|
187665
|
+
var _a, _b, _c;
|
|
187666
|
+
try {
|
|
187667
|
+
setCredentialsLoading(true);
|
|
187668
|
+
const response = await getUserCredentials({
|
|
187669
|
+
platformKey: getTenant(),
|
|
187670
|
+
username: (_a = getUserName()) !== null && _a !== void 0 ? _a : '',
|
|
187671
|
+
...(maxCredentials && { pageSize: maxCredentials }),
|
|
187672
|
+
}, true);
|
|
187673
|
+
const data = ((_c = (_b = response === null || response === void 0 ? void 0 : response.data) === null || _b === void 0 ? void 0 : _b.data) !== null && _c !== void 0 ? _c : []);
|
|
187674
|
+
setFetchedCredentials(data);
|
|
187675
|
+
setFilteredCredentials(data);
|
|
187676
|
+
}
|
|
187677
|
+
catch (_d) {
|
|
187678
|
+
setFetchedCredentials([]);
|
|
187679
|
+
setFilteredCredentials([]);
|
|
187680
|
+
}
|
|
187681
|
+
finally {
|
|
187682
|
+
setCredentialsLoading(false);
|
|
187683
|
+
}
|
|
187684
|
+
};
|
|
187685
|
+
useEffect(() => {
|
|
187686
|
+
handleFetchCredentials();
|
|
187687
|
+
}, []);
|
|
187688
|
+
useEffect(() => {
|
|
187689
|
+
if (search.length > 2) {
|
|
187690
|
+
setFilteredCredentials(fetchedCredentials.filter((credential) => {
|
|
187691
|
+
var _a;
|
|
187692
|
+
return String(((_a = credential === null || credential === void 0 ? void 0 : credential.credentialDetails) === null || _a === void 0 ? void 0 : _a.name) || '')
|
|
187693
|
+
.toLowerCase()
|
|
187694
|
+
.includes(search.toLowerCase());
|
|
187695
|
+
}));
|
|
187696
|
+
}
|
|
187697
|
+
else {
|
|
187698
|
+
setFilteredCredentials(fetchedCredentials);
|
|
187699
|
+
}
|
|
187700
|
+
}, [search, fetchedCredentials]);
|
|
187701
|
+
return {
|
|
187702
|
+
filteredCredentials,
|
|
187703
|
+
fetchedCredentials,
|
|
187704
|
+
isLoading: isLoading || credentialsLoading,
|
|
187705
|
+
isError,
|
|
187706
|
+
};
|
|
187707
|
+
};
|
|
187708
|
+
|
|
187709
|
+
var duration$1 = {exports: {}};
|
|
187710
|
+
|
|
187711
|
+
(function (module, exports) {
|
|
187712
|
+
!function(t,s){module.exports=s();}(commonjsGlobal,(function(){var t,s,n=1e3,i=6e4,e=36e5,r=864e5,o=/\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,u=31536e6,d=2628e6,a=/^(-|\+)?P(?:([-+]?[0-9,.]*)Y)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)W)?(?:([-+]?[0-9,.]*)D)?(?:T(?:([-+]?[0-9,.]*)H)?(?:([-+]?[0-9,.]*)M)?(?:([-+]?[0-9,.]*)S)?)?$/,h={years:u,months:d,days:r,hours:e,minutes:i,seconds:n,milliseconds:1,weeks:6048e5},c=function(t){return t instanceof g},f=function(t,s,n){return new g(t,n,s.$l)},m=function(t){return s.p(t)+"s"},l=function(t){return t<0},$=function(t){return l(t)?Math.ceil(t):Math.floor(t)},y=function(t){return Math.abs(t)},v=function(t,s){return t?l(t)?{negative:true,format:""+y(t)+s}:{negative:false,format:""+t+s}:{negative:false,format:""}},g=function(){function l(t,s,n){var i=this;if(this.$d={},this.$l=n,void 0===t&&(this.$ms=0,this.parseFromMilliseconds()),s)return f(t*h[m(s)],this);if("number"==typeof t)return this.$ms=t,this.parseFromMilliseconds(),this;if("object"==typeof t)return Object.keys(t).forEach((function(s){i.$d[m(s)]=t[s];})),this.calMilliseconds(),this;if("string"==typeof t){var e=t.match(a);if(e){var r=e.slice(2).map((function(t){return null!=t?Number(t):0}));return this.$d.years=r[0],this.$d.months=r[1],this.$d.weeks=r[2],this.$d.days=r[3],this.$d.hours=r[4],this.$d.minutes=r[5],this.$d.seconds=r[6],this.calMilliseconds(),this}}return this}var y=l.prototype;return y.calMilliseconds=function(){var t=this;this.$ms=Object.keys(this.$d).reduce((function(s,n){return s+(t.$d[n]||0)*h[n]}),0);},y.parseFromMilliseconds=function(){var t=this.$ms;this.$d.years=$(t/u),t%=u,this.$d.months=$(t/d),t%=d,this.$d.days=$(t/r),t%=r,this.$d.hours=$(t/e),t%=e,this.$d.minutes=$(t/i),t%=i,this.$d.seconds=$(t/n),t%=n,this.$d.milliseconds=t;},y.toISOString=function(){var t=v(this.$d.years,"Y"),s=v(this.$d.months,"M"),n=+this.$d.days||0;this.$d.weeks&&(n+=7*this.$d.weeks);var i=v(n,"D"),e=v(this.$d.hours,"H"),r=v(this.$d.minutes,"M"),o=this.$d.seconds||0;this.$d.milliseconds&&(o+=this.$d.milliseconds/1e3,o=Math.round(1e3*o)/1e3);var u=v(o,"S"),d=t.negative||s.negative||i.negative||e.negative||r.negative||u.negative,a=e.format||r.format||u.format?"T":"",h=(d?"-":"")+"P"+t.format+s.format+i.format+a+e.format+r.format+u.format;return "P"===h||"-P"===h?"P0D":h},y.toJSON=function(){return this.toISOString()},y.format=function(t){var n=t||"YYYY-MM-DDTHH:mm:ss",i={Y:this.$d.years,YY:s.s(this.$d.years,2,"0"),YYYY:s.s(this.$d.years,4,"0"),M:this.$d.months,MM:s.s(this.$d.months,2,"0"),D:this.$d.days,DD:s.s(this.$d.days,2,"0"),H:this.$d.hours,HH:s.s(this.$d.hours,2,"0"),m:this.$d.minutes,mm:s.s(this.$d.minutes,2,"0"),s:this.$d.seconds,ss:s.s(this.$d.seconds,2,"0"),SSS:s.s(this.$d.milliseconds,3,"0")};return n.replace(o,(function(t,s){return s||String(i[t])}))},y.as=function(t){return this.$ms/h[m(t)]},y.get=function(t){var s=this.$ms,n=m(t);return "milliseconds"===n?s%=1e3:s="weeks"===n?$(s/h[n]):this.$d[n],s||0},y.add=function(t,s,n){var i;return i=s?t*h[m(s)]:c(t)?t.$ms:f(t,this).$ms,f(this.$ms+i*(n?-1:1),this)},y.subtract=function(t,s){return this.add(t,s,true)},y.locale=function(t){var s=this.clone();return s.$l=t,s},y.clone=function(){return f(this.$ms,this)},y.humanize=function(s){return t().add(this.$ms,"ms").locale(this.$l).fromNow(!s)},y.valueOf=function(){return this.asMilliseconds()},y.milliseconds=function(){return this.get("milliseconds")},y.asMilliseconds=function(){return this.as("milliseconds")},y.seconds=function(){return this.get("seconds")},y.asSeconds=function(){return this.as("seconds")},y.minutes=function(){return this.get("minutes")},y.asMinutes=function(){return this.as("minutes")},y.hours=function(){return this.get("hours")},y.asHours=function(){return this.as("hours")},y.days=function(){return this.get("days")},y.asDays=function(){return this.as("days")},y.weeks=function(){return this.get("weeks")},y.asWeeks=function(){return this.as("weeks")},y.months=function(){return this.get("months")},y.asMonths=function(){return this.as("months")},y.years=function(){return this.get("years")},y.asYears=function(){return this.as("years")},l}(),p=function(t,s,n){return t.add(s.years()*n,"y").add(s.months()*n,"M").add(s.days()*n,"d").add(s.hours()*n,"h").add(s.minutes()*n,"m").add(s.seconds()*n,"s").add(s.milliseconds()*n,"ms")};return function(n,i,e){t=e,s=e().$utils(),e.duration=function(t,s){var n=e.locale();return f(t,{$l:n},s)},e.isDuration=c;var r=i.prototype.add,o=i.prototype.subtract;i.prototype.add=function(t,s){return c(t)?p(this,t,1):r.bind(this)(t,s)},i.prototype.subtract=function(t,s){return c(t)?p(this,t,-1):o.bind(this)(t,s)};}}));
|
|
187713
|
+
} (duration$1));
|
|
187714
|
+
|
|
187715
|
+
var durationExports = duration$1.exports;
|
|
187716
|
+
var duration = /*@__PURE__*/getDefaultExportFromCjs(durationExports);
|
|
187717
|
+
|
|
187718
|
+
dayjs.extend(duration);
|
|
187719
|
+
/**
|
|
187720
|
+
* Fetches the last 7 days of learning activity for the logged-in user
|
|
187721
|
+
* and returns it as a list of { date, minutes } rows ready for a chart.
|
|
187722
|
+
*
|
|
187723
|
+
* Ported from the skillsai SPA (hooks/profile/use-profile-timespent.ts).
|
|
187724
|
+
*/
|
|
187725
|
+
const useProfileTimeSpent = () => {
|
|
187726
|
+
const [timeSpent, setTimeSpent] = useState([]);
|
|
187727
|
+
const [timeSpentLoading, setTimeSpentLoading] = useState(false);
|
|
187728
|
+
const [getOverTimeActivity] = useLazyGetOverTimeActivityQuery();
|
|
187729
|
+
const handleTimeSpentStats = async () => {
|
|
187730
|
+
var _a, _b;
|
|
187731
|
+
setTimeSpentLoading(true);
|
|
187732
|
+
try {
|
|
187733
|
+
const response = await getOverTimeActivity([
|
|
187734
|
+
{
|
|
187735
|
+
org: getTenant(),
|
|
187736
|
+
userId: (_a = getUserName()) !== null && _a !== void 0 ? _a : '',
|
|
187737
|
+
startDate: dayjs().subtract(6, 'day').format('YYYY-MM-DD'),
|
|
187738
|
+
endDate: dayjs().format('YYYY-MM-DD'),
|
|
187739
|
+
format: 'json',
|
|
187740
|
+
},
|
|
187741
|
+
], true);
|
|
187742
|
+
const data = (_b = response === null || response === void 0 ? void 0 : response.data) === null || _b === void 0 ? void 0 : _b.data;
|
|
187743
|
+
if (!data || Object.keys(data).length === 0) {
|
|
187744
|
+
throw new Error('no data');
|
|
187745
|
+
}
|
|
187746
|
+
setTimeSpent(Object.entries(data).map(([date, seconds]) => ({
|
|
187747
|
+
date: dayjs(date).format('ddd DD/MM/YY'),
|
|
187748
|
+
minutes: dayjs.duration(seconds, 'seconds').asMinutes(),
|
|
187749
|
+
})));
|
|
187750
|
+
}
|
|
187751
|
+
catch (_c) {
|
|
187752
|
+
setTimeSpent([]);
|
|
187753
|
+
}
|
|
187754
|
+
finally {
|
|
187755
|
+
setTimeSpentLoading(false);
|
|
187756
|
+
}
|
|
187757
|
+
};
|
|
187758
|
+
useEffect(() => {
|
|
187759
|
+
handleTimeSpentStats();
|
|
187760
|
+
}, []);
|
|
187761
|
+
return { timeSpent, timeSpentLoading };
|
|
187762
|
+
};
|
|
187763
|
+
|
|
187764
|
+
/**
|
|
187765
|
+
* Thin wrapper around the catalog-search RTK endpoint that exposes a
|
|
187766
|
+
* promise-returning helper for ad-hoc searches inside effects/callbacks.
|
|
187767
|
+
*
|
|
187768
|
+
* Ported from the skillsai SPA (hooks/search/use-catalog-search.ts).
|
|
187769
|
+
*/
|
|
187770
|
+
const useCatalogSearch = () => {
|
|
187771
|
+
const [getCatalogSearch, { isLoading, isError }] = useLazyGetCatalogSearchQuery();
|
|
187772
|
+
const handleSearch = async (params) => {
|
|
187773
|
+
try {
|
|
187774
|
+
const response = await getCatalogSearch([{ ...params }], true);
|
|
187775
|
+
return response;
|
|
187776
|
+
}
|
|
187777
|
+
catch (error) {
|
|
187778
|
+
console.error(JSON.stringify(error));
|
|
187779
|
+
return null;
|
|
187780
|
+
}
|
|
187781
|
+
};
|
|
187782
|
+
return { handleSearch, isLoading, isError };
|
|
187783
|
+
};
|
|
187784
|
+
|
|
187785
|
+
/**
|
|
187786
|
+
* Aggregates the logged-in user's earned, self-reported, and desired
|
|
187787
|
+
* skills, plus mutations to create/update/delete self-reported and
|
|
187788
|
+
* desired skills. Also exposes a `handleFetchAllSkills` helper that
|
|
187789
|
+
* queries the catalog search endpoint for a skills list.
|
|
187790
|
+
*
|
|
187791
|
+
* Ported from the skillsai SPA (hooks/profile/use-profile-skills.ts).
|
|
187792
|
+
*/
|
|
187793
|
+
const useProfileSkills = (showToast = true) => {
|
|
187794
|
+
const { data: earnedSkills, isLoading: earnedSkillsLoading, isError: earnedSkillsError, isSuccess: earnedSkillsSuccess, } = useGetUserEarnedSkillsQuery([
|
|
187795
|
+
{ org: getTenant(), userId: getUserName() },
|
|
187796
|
+
]);
|
|
187797
|
+
const { data: selfReportedSkills, isLoading: selfReportedSkillsLoading, isError: selfReportedSkillsError, isSuccess: selfReportedSkillsSuccess, } = useGetUserReportedSkillsQuery([
|
|
187798
|
+
{ userId: getUserId(), username: getUserName() },
|
|
187799
|
+
]);
|
|
187800
|
+
const { data: desiredSkills, isLoading: desiredSkillsLoading, isError: desiredSkillsError, isSuccess: desiredSkillsSuccess, } = useGetUserDesiredSkillsQuery([
|
|
187801
|
+
{ userId: getUserId(), username: getUserName() },
|
|
187802
|
+
]);
|
|
187803
|
+
const { handleSearch, isLoading: isFetchingSkills, isError: isFetchingSkillsError, } = useCatalogSearch();
|
|
187804
|
+
const [updatingSkill, setUpdatingSkill] = useState(false);
|
|
187805
|
+
const [deletingSkill, setDeletingSkill] = useState(false);
|
|
187806
|
+
const [fetchedSkills, setFetchedSkills] = useState([]);
|
|
187807
|
+
const handleFetchAllSkills = async (searchQuery) => {
|
|
187808
|
+
var _a, _b;
|
|
187809
|
+
try {
|
|
187810
|
+
const result = await handleSearch({
|
|
187811
|
+
content: ['skills'],
|
|
187812
|
+
tenant: getTenant(),
|
|
187813
|
+
...(searchQuery && { query: searchQuery }),
|
|
187814
|
+
});
|
|
187815
|
+
if (!result) {
|
|
187816
|
+
throw new Error('Failed to fetch skills');
|
|
187817
|
+
}
|
|
187818
|
+
const items = (_b = (_a = result === null || result === void 0 ? void 0 : result.data) === null || _a === void 0 ? void 0 : _a.results) !== null && _b !== void 0 ? _b : [];
|
|
187819
|
+
setFetchedSkills(items);
|
|
187820
|
+
}
|
|
187821
|
+
catch (_c) {
|
|
187822
|
+
toast.error('Failed to fetch skills');
|
|
187823
|
+
setFetchedSkills([]);
|
|
187824
|
+
}
|
|
187825
|
+
};
|
|
187826
|
+
const [createOrUpdateUserReportedSkill, { isError: isCreatingOrUpdateUserReportedSkillError }] = useCreateOrUpdateUserReportedSkillMutation();
|
|
187827
|
+
const [createOrUpdateUserDesiredSkill, { isError: isCreatingOrUpdateUserDesiredSkillError }] = useCreateOrUpdateUserDesiredSkillMutation();
|
|
187828
|
+
const handleSkillsDeletion = async (skill, allSkills, callback) => {
|
|
187829
|
+
var _a, _b, _c, _d, _e, _f;
|
|
187830
|
+
setDeletingSkill(true);
|
|
187831
|
+
if (skill.type === 'self-reported') {
|
|
187832
|
+
const reportedSkillIndex = (_a = allSkills.selfReported) === null || _a === void 0 ? void 0 : _a.skills.findIndex((s) => s.name === skill.name);
|
|
187833
|
+
const updatedReportedLevel = (_b = allSkills.selfReported) === null || _b === void 0 ? void 0 : _b.data.level.filter((_, index) => index !== reportedSkillIndex);
|
|
187834
|
+
await createOrUpdateUserReportedSkill([
|
|
187835
|
+
{
|
|
187836
|
+
requestBody: {
|
|
187837
|
+
skills: ((_c = allSkills.selfReported) === null || _c === void 0 ? void 0 : _c.skills.filter((s) => s.name !== skill.name)) || [],
|
|
187838
|
+
data: { level: updatedReportedLevel },
|
|
187839
|
+
user_id: getUserId(),
|
|
187840
|
+
username: getUserName(),
|
|
187841
|
+
},
|
|
187842
|
+
},
|
|
187843
|
+
]);
|
|
187844
|
+
if (isCreatingOrUpdateUserReportedSkillError) {
|
|
187845
|
+
toast.error('Failed to delete skill');
|
|
187846
|
+
return;
|
|
187847
|
+
}
|
|
187848
|
+
toast.success('Skill deleted successfully');
|
|
187849
|
+
setTimeout(() => {
|
|
187850
|
+
setDeletingSkill(false);
|
|
187851
|
+
callback === null || callback === void 0 ? void 0 : callback();
|
|
187852
|
+
}, 1000);
|
|
187853
|
+
}
|
|
187854
|
+
else if (skill.type === 'desired') {
|
|
187855
|
+
const desiredSkillIndex = (_d = allSkills.desired) === null || _d === void 0 ? void 0 : _d.skills.findIndex((s) => s.name === skill.name);
|
|
187856
|
+
const updatedDesiredLevel = (_e = allSkills.desired) === null || _e === void 0 ? void 0 : _e.data.level.filter((_, index) => index !== desiredSkillIndex);
|
|
187857
|
+
await createOrUpdateUserDesiredSkill([
|
|
187858
|
+
{
|
|
187859
|
+
requestBody: {
|
|
187860
|
+
skills: ((_f = allSkills.desired) === null || _f === void 0 ? void 0 : _f.skills.filter((s) => s.name !== skill.name)) || [],
|
|
187861
|
+
data: { level: updatedDesiredLevel },
|
|
187862
|
+
user_id: getUserId(),
|
|
187863
|
+
username: getUserName(),
|
|
187864
|
+
},
|
|
187865
|
+
},
|
|
187866
|
+
]);
|
|
187867
|
+
if (isCreatingOrUpdateUserDesiredSkillError) {
|
|
187868
|
+
toast.error('Failed to delete skill');
|
|
187869
|
+
return;
|
|
187870
|
+
}
|
|
187871
|
+
toast.success('Skill deleted successfully');
|
|
187872
|
+
setTimeout(() => {
|
|
187873
|
+
setDeletingSkill(false);
|
|
187874
|
+
callback === null || callback === void 0 ? void 0 : callback();
|
|
187875
|
+
}, 1000);
|
|
187876
|
+
}
|
|
187877
|
+
};
|
|
187878
|
+
const handleSkillsUpdate = async (skill, allSkills, callback) => {
|
|
187879
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
187880
|
+
setUpdatingSkill(true);
|
|
187881
|
+
if (skill.type === 'self-reported') {
|
|
187882
|
+
const reportedSkillIndex = (_a = allSkills.selfReported) === null || _a === void 0 ? void 0 : _a.skills.findIndex((s) => s.name === skill.name);
|
|
187883
|
+
const updatedReportedLevel = [...((_c = (_b = allSkills.selfReported) === null || _b === void 0 ? void 0 : _b.data.level) !== null && _c !== void 0 ? _c : [])];
|
|
187884
|
+
if (reportedSkillIndex !== undefined &&
|
|
187885
|
+
reportedSkillIndex > -1 &&
|
|
187886
|
+
skill.level !== undefined) {
|
|
187887
|
+
updatedReportedLevel[reportedSkillIndex] = skill.level;
|
|
187888
|
+
}
|
|
187889
|
+
await createOrUpdateUserReportedSkill([
|
|
187890
|
+
{
|
|
187891
|
+
requestBody: {
|
|
187892
|
+
skills: ((_d = allSkills.selfReported) === null || _d === void 0 ? void 0 : _d.skills) || [],
|
|
187893
|
+
data: { level: updatedReportedLevel },
|
|
187894
|
+
user_id: getUserId(),
|
|
187895
|
+
username: getUserName(),
|
|
187896
|
+
},
|
|
187897
|
+
},
|
|
187898
|
+
]);
|
|
187899
|
+
if (isCreatingOrUpdateUserReportedSkillError) {
|
|
187900
|
+
toast.error('Failed to update skill');
|
|
187901
|
+
return;
|
|
187902
|
+
}
|
|
187903
|
+
toast.success('Skill updated successfully');
|
|
187904
|
+
setTimeout(() => {
|
|
187905
|
+
setUpdatingSkill(false);
|
|
187906
|
+
callback === null || callback === void 0 ? void 0 : callback();
|
|
187907
|
+
}, 1000);
|
|
187908
|
+
}
|
|
187909
|
+
else if (skill.type === 'desired') {
|
|
187910
|
+
const desiredSkillIndex = (_e = allSkills.desired) === null || _e === void 0 ? void 0 : _e.skills.findIndex((s) => s.name === skill.name);
|
|
187911
|
+
const updatedDesiredLevel = [...((_g = (_f = allSkills.desired) === null || _f === void 0 ? void 0 : _f.data.level) !== null && _g !== void 0 ? _g : [])];
|
|
187912
|
+
if (desiredSkillIndex !== undefined && desiredSkillIndex > -1 && skill.level !== undefined) {
|
|
187913
|
+
updatedDesiredLevel[desiredSkillIndex] = skill.level;
|
|
187914
|
+
}
|
|
187915
|
+
await createOrUpdateUserDesiredSkill([
|
|
187916
|
+
{
|
|
187917
|
+
requestBody: {
|
|
187918
|
+
skills: ((_h = allSkills.desired) === null || _h === void 0 ? void 0 : _h.skills) || [],
|
|
187919
|
+
data: { level: updatedDesiredLevel },
|
|
187920
|
+
user_id: getUserId(),
|
|
187921
|
+
username: getUserName(),
|
|
187922
|
+
},
|
|
187923
|
+
},
|
|
187924
|
+
]);
|
|
187925
|
+
if (isCreatingOrUpdateUserDesiredSkillError) {
|
|
187926
|
+
toast.error('Failed to update skill');
|
|
187927
|
+
return;
|
|
187928
|
+
}
|
|
187929
|
+
toast.success('Skill updated successfully');
|
|
187930
|
+
setTimeout(() => {
|
|
187931
|
+
setUpdatingSkill(false);
|
|
187932
|
+
callback === null || callback === void 0 ? void 0 : callback();
|
|
187933
|
+
}, 1000);
|
|
187934
|
+
}
|
|
187935
|
+
};
|
|
187936
|
+
const handleSkillsCreate = async (skills) => {
|
|
187937
|
+
try {
|
|
187938
|
+
await createOrUpdateUserReportedSkill([
|
|
187939
|
+
{ requestBody: skills, userId: getUserId(), username: getUserName() },
|
|
187940
|
+
]);
|
|
187941
|
+
if (isCreatingOrUpdateUserReportedSkillError) {
|
|
187942
|
+
throw new Error('Failed to create skills');
|
|
187943
|
+
}
|
|
187944
|
+
if (showToast) {
|
|
187945
|
+
toast.success('Skills created successfully');
|
|
187946
|
+
}
|
|
187947
|
+
return true;
|
|
187948
|
+
}
|
|
187949
|
+
catch (_a) {
|
|
187950
|
+
toast.error('Failed to create skills');
|
|
187951
|
+
return false;
|
|
187952
|
+
}
|
|
187953
|
+
};
|
|
187954
|
+
return {
|
|
187955
|
+
earnedSkills,
|
|
187956
|
+
earnedSkillsLoading,
|
|
187957
|
+
earnedSkillsError,
|
|
187958
|
+
earnedSkillsSuccess,
|
|
187959
|
+
selfReportedSkills,
|
|
187960
|
+
selfReportedSkillsLoading,
|
|
187961
|
+
selfReportedSkillsError,
|
|
187962
|
+
selfReportedSkillsSuccess,
|
|
187963
|
+
desiredSkills,
|
|
187964
|
+
desiredSkillsLoading,
|
|
187965
|
+
desiredSkillsError,
|
|
187966
|
+
desiredSkillsSuccess,
|
|
187967
|
+
handleSkillsDeletion,
|
|
187968
|
+
updatingSkill,
|
|
187969
|
+
deletingSkill,
|
|
187970
|
+
setUpdatingSkill,
|
|
187971
|
+
handleSkillsUpdate,
|
|
187972
|
+
handleSkillsCreate,
|
|
187973
|
+
handleFetchAllSkills,
|
|
187974
|
+
fetchedSkills,
|
|
187975
|
+
isFetchingSkills,
|
|
187976
|
+
isFetchingSkillsError,
|
|
187977
|
+
};
|
|
187978
|
+
};
|
|
187979
|
+
|
|
187980
|
+
/**
|
|
187981
|
+
* Fetches the logged-in user's catalog / assigned / enrolled pathways and
|
|
187982
|
+
* hydrates each with its completion stats. Supports client-side search
|
|
187983
|
+
* filtering on pathway name.
|
|
187984
|
+
*
|
|
187985
|
+
* Ported from the skillsai SPA (hooks/profile/use-profile-pathways.ts).
|
|
187986
|
+
*/
|
|
187987
|
+
const useProfilePathways = ({ searchQuery, contentType = 'catalog', lmsUrl, }) => {
|
|
187988
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
187989
|
+
const [getPathwayList, { isError: isCatalogPathwaysError }] = useLazyGetPathwayListQuery();
|
|
187990
|
+
const [getUserAssignedPathways, { isError: isAssignedPathwaysError }] = useLazyGetUserAssignedPathwaysQuery();
|
|
187991
|
+
const [getUserEnrolledPathways, { isError: isEnrolledPathwaysError }] = useLazyGetUserEnrolledPathwaysQuery();
|
|
187992
|
+
const [getPathwayCompletion] = useLazyGetPathwayCompletionQuery();
|
|
187993
|
+
const [pathways, setPathways] = useState([]);
|
|
187994
|
+
const [filteredPathways, setFilteredPathways] = useState([]);
|
|
187995
|
+
const [pathwayCompletions, setPathwayCompletions] = useState([]);
|
|
187996
|
+
const [pathwayCompletionsLoading, setPathwayCompletionsLoading] = useState(false);
|
|
187997
|
+
const [isError, setIsError] = useState(false);
|
|
187998
|
+
const handleFetchSinglePathwayEnrollmentStatus = async (pathway) => {
|
|
187999
|
+
try {
|
|
188000
|
+
const response = await getUserEnrolledPathways([
|
|
188001
|
+
{ username: getUserName(), pathwayUuid: pathway.pathway_uuid || '' },
|
|
188002
|
+
], true);
|
|
188003
|
+
const data = response.data;
|
|
188004
|
+
return (Array.isArray(data) &&
|
|
188005
|
+
data.findIndex((pre) => pre.active && pre.pathway_uuid === pathway.pathway_uuid) !== -1);
|
|
188006
|
+
}
|
|
188007
|
+
catch (_a) {
|
|
188008
|
+
return false;
|
|
188009
|
+
}
|
|
188010
|
+
};
|
|
188011
|
+
const handleFetchPathwayCompletions = async (list) => {
|
|
188012
|
+
setPathwayCompletionsLoading(true);
|
|
188013
|
+
try {
|
|
188014
|
+
const completions = await Promise.all(list.map(async (pathway) => {
|
|
188015
|
+
const response = await getPathwayCompletion([
|
|
188016
|
+
{ pathwayUuid: pathway.pathway_uuid || '', username: getUserName() },
|
|
188017
|
+
]);
|
|
188018
|
+
return (response.data || {});
|
|
188019
|
+
}));
|
|
188020
|
+
setPathwayCompletions(completions);
|
|
188021
|
+
}
|
|
188022
|
+
catch (error) {
|
|
188023
|
+
console.error(JSON.stringify(error));
|
|
188024
|
+
setPathwayCompletions([]);
|
|
188025
|
+
}
|
|
188026
|
+
finally {
|
|
188027
|
+
setPathwayCompletionsLoading(false);
|
|
188028
|
+
}
|
|
188029
|
+
};
|
|
188030
|
+
const handleFetchCatalogPathways = async () => {
|
|
188031
|
+
var _a;
|
|
188032
|
+
setIsLoading(true);
|
|
188033
|
+
const response = await getPathwayList([{ username: getUserName(), platformKey: getTenant() }], true);
|
|
188034
|
+
const raw = (_a = response.data) !== null && _a !== void 0 ? _a : [];
|
|
188035
|
+
const fetchedPathways = raw.map((result) => {
|
|
188036
|
+
const metadata = result.metadata;
|
|
188037
|
+
return {
|
|
188038
|
+
...result,
|
|
188039
|
+
metadata: {
|
|
188040
|
+
...metadata,
|
|
188041
|
+
course_image_asset_path: (metadata === null || metadata === void 0 ? void 0 : metadata.course_image_asset_path)
|
|
188042
|
+
? (lmsUrl !== null && lmsUrl !== void 0 ? lmsUrl : '') + metadata.course_image_asset_path
|
|
188043
|
+
: getRandomCourseImage(),
|
|
188044
|
+
},
|
|
188045
|
+
};
|
|
188046
|
+
});
|
|
188047
|
+
setPathways(fetchedPathways);
|
|
188048
|
+
setFilteredPathways(fetchedPathways);
|
|
188049
|
+
handleFetchPathwayCompletions(fetchedPathways);
|
|
188050
|
+
setIsError(isCatalogPathwaysError);
|
|
188051
|
+
setIsLoading(false);
|
|
188052
|
+
};
|
|
188053
|
+
const handleFetchAssignedPathways = async () => {
|
|
188054
|
+
var _a, _b;
|
|
188055
|
+
setIsLoading(true);
|
|
188056
|
+
const userId = getUserId();
|
|
188057
|
+
const response = await getUserAssignedPathways({ user_id: userId }, true);
|
|
188058
|
+
const results = ((_b = (_a = response.data) === null || _a === void 0 ? void 0 : _a.results) !== null && _b !== void 0 ? _b : []) || [];
|
|
188059
|
+
setPathways(results);
|
|
188060
|
+
setFilteredPathways(results);
|
|
188061
|
+
handleFetchPathwayCompletions(results);
|
|
188062
|
+
setIsError(isAssignedPathwaysError);
|
|
188063
|
+
setIsLoading(false);
|
|
188064
|
+
};
|
|
188065
|
+
const handleFetchEnrolledPathways = async () => {
|
|
188066
|
+
setIsLoading(true);
|
|
188067
|
+
const response = await getUserEnrolledPathways([{ username: getUserName() }], true);
|
|
188068
|
+
const data = response.data || [];
|
|
188069
|
+
setPathways(data);
|
|
188070
|
+
setFilteredPathways(data);
|
|
188071
|
+
handleFetchPathwayCompletions(data);
|
|
188072
|
+
setIsError(isEnrolledPathwaysError);
|
|
188073
|
+
setIsLoading(false);
|
|
188074
|
+
};
|
|
188075
|
+
useEffect(() => {
|
|
188076
|
+
setPathways([]);
|
|
188077
|
+
setFilteredPathways([]);
|
|
188078
|
+
switch (contentType) {
|
|
188079
|
+
case 'assigned':
|
|
188080
|
+
handleFetchAssignedPathways();
|
|
188081
|
+
break;
|
|
188082
|
+
case 'enrolled':
|
|
188083
|
+
handleFetchEnrolledPathways();
|
|
188084
|
+
break;
|
|
188085
|
+
default:
|
|
188086
|
+
handleFetchCatalogPathways();
|
|
188087
|
+
break;
|
|
188088
|
+
}
|
|
188089
|
+
}, [contentType]);
|
|
188090
|
+
useEffect(() => {
|
|
188091
|
+
if (String(searchQuery).length > 2) {
|
|
188092
|
+
setFilteredPathways(pathways.filter((pathway) => { var _a; return (_a = pathway.name) === null || _a === void 0 ? void 0 : _a.toLowerCase().includes(searchQuery.toLowerCase()); }));
|
|
188093
|
+
}
|
|
188094
|
+
else {
|
|
188095
|
+
setFilteredPathways(pathways);
|
|
188096
|
+
}
|
|
188097
|
+
}, [searchQuery]);
|
|
188098
|
+
return {
|
|
188099
|
+
pathways,
|
|
188100
|
+
filteredPathways,
|
|
188101
|
+
isLoading,
|
|
188102
|
+
isError,
|
|
188103
|
+
setPathways,
|
|
188104
|
+
setFilteredPathways,
|
|
188105
|
+
pathwayCompletions,
|
|
188106
|
+
pathwayCompletionsLoading,
|
|
188107
|
+
handleFetchSinglePathwayEnrollmentStatus,
|
|
188108
|
+
};
|
|
188109
|
+
};
|
|
188110
|
+
|
|
188111
|
+
/**
|
|
188112
|
+
* Fetches the logged-in user's enrolled / assigned / catalog programs
|
|
188113
|
+
* and hydrates each with its completion stats. Supports client-side
|
|
188114
|
+
* search filtering on program name.
|
|
188115
|
+
*
|
|
188116
|
+
* Ported from the skillsai SPA (hooks/profile/use-profile-programs.ts).
|
|
188117
|
+
*/
|
|
188118
|
+
const useProfilePrograms = ({ searchQuery, activeTab = 'enrolled', }) => {
|
|
188119
|
+
const [programs, setPrograms] = useState([]);
|
|
188120
|
+
const [filteredPrograms, setFilteredPrograms] = useState([]);
|
|
188121
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
188122
|
+
const [isError, setIsError] = useState(false);
|
|
188123
|
+
const [programCompletions, setProgramCompletions] = useState([]);
|
|
188124
|
+
const [programCompletionsLoading, setProgramCompletionsLoading] = useState(false);
|
|
188125
|
+
const [getProgramList, { isError: isCatalogProgramsError }] = useLazyGetProgramListQuery();
|
|
188126
|
+
const [getProgramCompletion] = useLazyGetProgramCompletionQuery();
|
|
188127
|
+
const [getUserEnrolledPrograms, { isError: isEnrolledProgramsError }] = useLazyGetUserEnrolledProgramsQuery();
|
|
188128
|
+
const [getAssignedPrograms, { isError: isAssignedProgramsError }] = useLazyGetAssignedProgramsQuery();
|
|
188129
|
+
const withImage = (program) => {
|
|
188130
|
+
var _a;
|
|
188131
|
+
return ({
|
|
188132
|
+
...program,
|
|
188133
|
+
metadata: {
|
|
188134
|
+
...((_a = program.metadata) !== null && _a !== void 0 ? _a : {}),
|
|
188135
|
+
image: getRandomCourseImage(),
|
|
188136
|
+
},
|
|
188137
|
+
});
|
|
188138
|
+
};
|
|
188139
|
+
const handleFetchProgramCompletions = async (list) => {
|
|
188140
|
+
setProgramCompletionsLoading(true);
|
|
188141
|
+
try {
|
|
188142
|
+
const completions = await Promise.all(list.map(async (program) => {
|
|
188143
|
+
const response = await getProgramCompletion([
|
|
188144
|
+
{ programKey: program.program_key || '', username: getUserName() },
|
|
188145
|
+
], true);
|
|
188146
|
+
return (response.data || {});
|
|
188147
|
+
}));
|
|
188148
|
+
setProgramCompletions(completions);
|
|
188149
|
+
}
|
|
188150
|
+
catch (error) {
|
|
188151
|
+
console.error(JSON.stringify(error));
|
|
188152
|
+
setProgramCompletions([]);
|
|
188153
|
+
}
|
|
188154
|
+
finally {
|
|
188155
|
+
setProgramCompletionsLoading(false);
|
|
188156
|
+
}
|
|
188157
|
+
};
|
|
188158
|
+
const handleProgramEnrollmentFetch = async () => {
|
|
188159
|
+
var _a;
|
|
188160
|
+
setIsLoading(true);
|
|
188161
|
+
const response = await getUserEnrolledPrograms([{ userId: getUserId(), platformKey: getTenant() }], true);
|
|
188162
|
+
const raw = (_a = response.data) !== null && _a !== void 0 ? _a : [];
|
|
188163
|
+
const fetched = raw.map(withImage);
|
|
188164
|
+
setPrograms(fetched);
|
|
188165
|
+
setFilteredPrograms(fetched);
|
|
188166
|
+
handleFetchProgramCompletions(fetched);
|
|
188167
|
+
setIsError(isEnrolledProgramsError);
|
|
188168
|
+
setIsLoading(false);
|
|
188169
|
+
};
|
|
188170
|
+
const handleAssignedProgramsFetch = async () => {
|
|
188171
|
+
var _a, _b;
|
|
188172
|
+
setIsLoading(true);
|
|
188173
|
+
const response = await getAssignedPrograms({ user_id: getUserId() }, true);
|
|
188174
|
+
const raw = ((_b = (_a = response.data) === null || _a === void 0 ? void 0 : _a.results) !== null && _b !== void 0 ? _b : []) || [];
|
|
188175
|
+
const fetched = raw.map(withImage);
|
|
188176
|
+
setPrograms(fetched);
|
|
188177
|
+
setFilteredPrograms(fetched);
|
|
188178
|
+
handleFetchProgramCompletions(fetched);
|
|
188179
|
+
setIsError(isAssignedProgramsError);
|
|
188180
|
+
setIsLoading(false);
|
|
188181
|
+
};
|
|
188182
|
+
const handleFetchCatalogPrograms = async () => {
|
|
188183
|
+
var _a;
|
|
188184
|
+
setIsLoading(true);
|
|
188185
|
+
const response = await getProgramList([{ org: getOrg(), username: getUserName() }], true);
|
|
188186
|
+
const raw = (_a = response.data) !== null && _a !== void 0 ? _a : [];
|
|
188187
|
+
const fetched = raw.map(withImage);
|
|
188188
|
+
setPrograms(fetched);
|
|
188189
|
+
setFilteredPrograms(fetched);
|
|
188190
|
+
handleFetchProgramCompletions(fetched);
|
|
188191
|
+
setIsError(isCatalogProgramsError);
|
|
188192
|
+
setIsLoading(false);
|
|
188193
|
+
};
|
|
188194
|
+
useEffect(() => {
|
|
188195
|
+
if (String(searchQuery).length > 2) {
|
|
188196
|
+
setFilteredPrograms(programs.filter((program) => { var _a; return (_a = program.name) === null || _a === void 0 ? void 0 : _a.toLowerCase().includes(searchQuery.toLowerCase()); }));
|
|
188197
|
+
}
|
|
188198
|
+
else {
|
|
188199
|
+
setFilteredPrograms(programs);
|
|
188200
|
+
}
|
|
188201
|
+
}, [searchQuery]);
|
|
188202
|
+
useEffect(() => {
|
|
188203
|
+
switch (activeTab) {
|
|
188204
|
+
case 'assigned':
|
|
188205
|
+
handleAssignedProgramsFetch();
|
|
188206
|
+
break;
|
|
188207
|
+
case 'enrolled':
|
|
188208
|
+
handleProgramEnrollmentFetch();
|
|
188209
|
+
break;
|
|
188210
|
+
case 'catalog':
|
|
188211
|
+
handleFetchCatalogPrograms();
|
|
188212
|
+
break;
|
|
188213
|
+
}
|
|
188214
|
+
}, [activeTab]);
|
|
188215
|
+
return {
|
|
188216
|
+
programs,
|
|
188217
|
+
filteredPrograms,
|
|
188218
|
+
isLoading,
|
|
188219
|
+
isError,
|
|
188220
|
+
setFilteredPrograms,
|
|
188221
|
+
setPrograms,
|
|
188222
|
+
programCompletions,
|
|
188223
|
+
programCompletionsLoading,
|
|
188224
|
+
};
|
|
188225
|
+
};
|
|
188226
|
+
|
|
188227
|
+
const isEmpty = (value) => {
|
|
188228
|
+
if (value == null)
|
|
188229
|
+
return true;
|
|
188230
|
+
if (Array.isArray(value) || typeof value === 'string')
|
|
188231
|
+
return value.length === 0;
|
|
188232
|
+
if (typeof value === 'object')
|
|
188233
|
+
return Object.keys(value).length === 0;
|
|
188234
|
+
return false;
|
|
188235
|
+
};
|
|
188236
|
+
/**
|
|
188237
|
+
* Aggregates a fixed list of profile activity counters (points, skills,
|
|
188238
|
+
* credentials, courses, programs, pathways, resources, assessments,
|
|
188239
|
+
* videos) by fanning out to nine RTK lazy queries and updating each row
|
|
188240
|
+
* independently as its request resolves. Errors on any single fetch
|
|
188241
|
+
* collapse that stat to 0 without blocking the others.
|
|
188242
|
+
*
|
|
188243
|
+
* Ported from the skillsai SPA (hooks/profile/use-profile-activity-stats.ts).
|
|
188244
|
+
*/
|
|
188245
|
+
const useProfileActivityStats = () => {
|
|
188246
|
+
const [getUserSkillsPoints, { isError: isErrorGetUserSkillsPoints }] = useLazyGetUserSkillsPointsQuery();
|
|
188247
|
+
const [getUserReportedSkills, { isError: isErrorGetUserReportedSkills }] = useLazyGetUserReportedSkillsQuery();
|
|
188248
|
+
const [getUserDesiredSkills, { isError: isErrorGetUserDesiredSkills }] = useLazyGetUserDesiredSkillsQuery();
|
|
188249
|
+
const [getUserCredentials, { isError: isErrorGetUserCredentials }] = useLazyGetUserCredentialsQuery();
|
|
188250
|
+
const [getUserEnrolledCourses, { isError: isErrorGetUserEnrolledCourses }] = useLazyGetUserEnrolledCoursesQuery();
|
|
188251
|
+
const [getUserEnrolledPrograms, { isError: isEnrolledProgramsError }] = useLazyGetUserEnrolledProgramsQuery();
|
|
188252
|
+
const [getUserCatalogPathways, { isError: isCatalogPathwaysError }] = useLazyGetUserCatalogPathwaysQuery();
|
|
188253
|
+
const [getPerLearnerInfo, { isError: isErrorgetPerLearnerInfo }] = useLazyGetPerLearnerInfoQuery();
|
|
188254
|
+
const [stats, setStats] = useState([
|
|
188255
|
+
{ value: 0, label: 'Points', loading: true },
|
|
188256
|
+
{ value: 3, label: 'Skills', loading: true },
|
|
188257
|
+
{ value: 0, label: 'Credentials', loading: true },
|
|
188258
|
+
{ value: 4, label: 'Courses', loading: true },
|
|
188259
|
+
{ value: 0, label: 'Programs', loading: true },
|
|
188260
|
+
{ value: 0, label: 'Pathways', loading: true },
|
|
188261
|
+
{ value: 0, label: 'Resources', loading: true },
|
|
188262
|
+
{ value: 0, label: 'Assessments', loading: true },
|
|
188263
|
+
{ value: 0, label: 'Videos', loading: true },
|
|
188264
|
+
]);
|
|
188265
|
+
const updateSingleStat = (stat) => {
|
|
188266
|
+
setStats((old) => old.map((s) => (s.label === stat.label ? stat : s)));
|
|
188267
|
+
};
|
|
188268
|
+
const handleSkillsPointActivityStats = async () => {
|
|
188269
|
+
var _a;
|
|
188270
|
+
const label = 'Points';
|
|
188271
|
+
try {
|
|
188272
|
+
const response = await getUserSkillsPoints([{ userId: getUserId(), username: getUserName() }], true);
|
|
188273
|
+
const skillPoints = (_a = response === null || response === void 0 ? void 0 : response.data) === null || _a === void 0 ? void 0 : _a.skill_points;
|
|
188274
|
+
if (isErrorGetUserSkillsPoints || isEmpty(skillPoints))
|
|
188275
|
+
throw new Error();
|
|
188276
|
+
const total = Object.values(skillPoints || {}).reduce((sum, skill) => { var _a; return sum + ((_a = skill.total_points) !== null && _a !== void 0 ? _a : 0); }, 0);
|
|
188277
|
+
updateSingleStat({ value: total, label, loading: false });
|
|
188278
|
+
}
|
|
188279
|
+
catch (_b) {
|
|
188280
|
+
updateSingleStat({ value: 0, label, loading: false });
|
|
188281
|
+
}
|
|
188282
|
+
};
|
|
188283
|
+
const handleActivitySkillsStats = async () => {
|
|
188284
|
+
var _a, _b, _c, _d, _e, _f;
|
|
188285
|
+
const label = 'Skills';
|
|
188286
|
+
try {
|
|
188287
|
+
const reportedSkills = await getUserReportedSkills([{ userId: getUserId(), username: getUserName() }], true);
|
|
188288
|
+
if (isErrorGetUserDesiredSkills && isErrorGetUserReportedSkills)
|
|
188289
|
+
throw new Error();
|
|
188290
|
+
let skillsCount = 0;
|
|
188291
|
+
if (!isErrorGetUserReportedSkills) {
|
|
188292
|
+
skillsCount = ((_c = (_b = (_a = reportedSkills === null || reportedSkills === void 0 ? void 0 : reportedSkills.data) === null || _a === void 0 ? void 0 : _a.skills) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0);
|
|
188293
|
+
}
|
|
188294
|
+
const desiredSkills = await getUserDesiredSkills([{ userId: getUserId(), username: getUserName() }], true);
|
|
188295
|
+
if (!isErrorGetUserDesiredSkills) {
|
|
188296
|
+
skillsCount += ((_f = (_e = (_d = desiredSkills === null || desiredSkills === void 0 ? void 0 : desiredSkills.data) === null || _d === void 0 ? void 0 : _d.skills) === null || _e === void 0 ? void 0 : _e.length) !== null && _f !== void 0 ? _f : 0);
|
|
188297
|
+
}
|
|
188298
|
+
updateSingleStat({ value: skillsCount, label, loading: false });
|
|
188299
|
+
}
|
|
188300
|
+
catch (_g) {
|
|
188301
|
+
updateSingleStat({ value: 0, label, loading: false });
|
|
188302
|
+
}
|
|
188303
|
+
};
|
|
188304
|
+
const handleCredentialsActivityStats = async () => {
|
|
188305
|
+
var _a, _b, _c;
|
|
188306
|
+
const label = 'Credentials';
|
|
188307
|
+
try {
|
|
188308
|
+
const response = await getUserCredentials({
|
|
188309
|
+
platformKey: getTenant(),
|
|
188310
|
+
username: (_a = getUserName()) !== null && _a !== void 0 ? _a : '',
|
|
188311
|
+
}, true);
|
|
188312
|
+
if (isErrorGetUserCredentials || isEmpty(response === null || response === void 0 ? void 0 : response.data))
|
|
188313
|
+
throw new Error();
|
|
188314
|
+
const data = (_b = response === null || response === void 0 ? void 0 : response.data) === null || _b === void 0 ? void 0 : _b.data;
|
|
188315
|
+
updateSingleStat({ value: (_c = data === null || data === void 0 ? void 0 : data.length) !== null && _c !== void 0 ? _c : 0, label, loading: false });
|
|
188316
|
+
}
|
|
188317
|
+
catch (_d) {
|
|
188318
|
+
updateSingleStat({ value: 0, label, loading: false });
|
|
188319
|
+
}
|
|
188320
|
+
};
|
|
188321
|
+
const handleCoursesActivityStats = async () => {
|
|
188322
|
+
var _a, _b;
|
|
188323
|
+
const label = 'Courses';
|
|
188324
|
+
try {
|
|
188325
|
+
const username = getUserName();
|
|
188326
|
+
if (!username)
|
|
188327
|
+
throw new Error();
|
|
188328
|
+
const response = await getUserEnrolledCourses({ username }, true);
|
|
188329
|
+
if (isErrorGetUserEnrolledCourses || isEmpty(response.data))
|
|
188330
|
+
throw new Error();
|
|
188331
|
+
updateSingleStat({ value: (_b = (_a = response === null || response === void 0 ? void 0 : response.data) === null || _a === void 0 ? void 0 : _a.count) !== null && _b !== void 0 ? _b : 0, label, loading: false });
|
|
188332
|
+
}
|
|
188333
|
+
catch (_c) {
|
|
188334
|
+
updateSingleStat({ value: 0, label, loading: false });
|
|
188335
|
+
}
|
|
188336
|
+
};
|
|
188337
|
+
const handleProgramsActivityStats = async () => {
|
|
188338
|
+
var _a;
|
|
188339
|
+
const label = 'Programs';
|
|
188340
|
+
try {
|
|
188341
|
+
const response = await getUserEnrolledPrograms({ username: getUserName(), platform_key: getTenant() }, true);
|
|
188342
|
+
if (isEnrolledProgramsError || isEmpty(response.data))
|
|
188343
|
+
throw new Error();
|
|
188344
|
+
const data = response === null || response === void 0 ? void 0 : response.data;
|
|
188345
|
+
updateSingleStat({ value: (_a = data === null || data === void 0 ? void 0 : data.length) !== null && _a !== void 0 ? _a : 0, label, loading: false });
|
|
188346
|
+
}
|
|
188347
|
+
catch (_b) {
|
|
188348
|
+
updateSingleStat({ value: 0, label, loading: false });
|
|
188349
|
+
}
|
|
188350
|
+
};
|
|
188351
|
+
const handlePathwaysActivityStats = async () => {
|
|
188352
|
+
var _a;
|
|
188353
|
+
const pathwaysLabel = 'Pathways';
|
|
188354
|
+
const resourcesLabel = 'Resources';
|
|
188355
|
+
try {
|
|
188356
|
+
const response = await getUserCatalogPathways({ username: (_a = getUserName()) !== null && _a !== void 0 ? _a : '', platform_key: getTenant() }, true);
|
|
188357
|
+
if (isCatalogPathwaysError || isEmpty(response.data))
|
|
188358
|
+
throw new Error();
|
|
188359
|
+
const data = response.data;
|
|
188360
|
+
const seen = new Set();
|
|
188361
|
+
const unique = data.filter((item) => {
|
|
188362
|
+
const dup = seen.has(item.id);
|
|
188363
|
+
seen.add(item.id);
|
|
188364
|
+
return !dup;
|
|
188365
|
+
});
|
|
188366
|
+
const totalResources = unique.reduce((sum, pathway) => { var _a, _b; return sum + ((_b = (_a = pathway.path) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0); }, 0);
|
|
188367
|
+
updateSingleStat({ value: unique.length, label: pathwaysLabel, loading: false });
|
|
188368
|
+
updateSingleStat({ value: totalResources, label: resourcesLabel, loading: false });
|
|
188369
|
+
}
|
|
188370
|
+
catch (_b) {
|
|
188371
|
+
updateSingleStat({ value: 0, label: pathwaysLabel, loading: false });
|
|
188372
|
+
updateSingleStat({ value: 0, label: resourcesLabel, loading: false });
|
|
188373
|
+
}
|
|
188374
|
+
};
|
|
188375
|
+
const handlePerLearnerInfoStats = async () => {
|
|
188376
|
+
var _a, _b, _c;
|
|
188377
|
+
const assessmentsLabel = 'Assessments';
|
|
188378
|
+
const videosLabel = 'Videos';
|
|
188379
|
+
try {
|
|
188380
|
+
const response = await getPerLearnerInfo([
|
|
188381
|
+
{
|
|
188382
|
+
org: getTenant(),
|
|
188383
|
+
userId: getUserName(),
|
|
188384
|
+
format: 'json',
|
|
188385
|
+
includeMainPlatform: true,
|
|
188386
|
+
},
|
|
188387
|
+
], true);
|
|
188388
|
+
if (isErrorgetPerLearnerInfo || isEmpty(response.data))
|
|
188389
|
+
throw new Error();
|
|
188390
|
+
const inner = (_a = response === null || response === void 0 ? void 0 : response.data) === null || _a === void 0 ? void 0 : _a.data;
|
|
188391
|
+
updateSingleStat({
|
|
188392
|
+
value: (_b = inner === null || inner === void 0 ? void 0 : inner.total_assessments) !== null && _b !== void 0 ? _b : 0,
|
|
188393
|
+
label: assessmentsLabel,
|
|
188394
|
+
loading: false,
|
|
188395
|
+
});
|
|
188396
|
+
updateSingleStat({ value: (_c = inner === null || inner === void 0 ? void 0 : inner.total_videos) !== null && _c !== void 0 ? _c : 0, label: videosLabel, loading: false });
|
|
188397
|
+
}
|
|
188398
|
+
catch (_d) {
|
|
188399
|
+
updateSingleStat({ value: 0, label: assessmentsLabel, loading: false });
|
|
188400
|
+
updateSingleStat({ value: 0, label: videosLabel, loading: false });
|
|
188401
|
+
}
|
|
188402
|
+
};
|
|
188403
|
+
useEffect(() => {
|
|
188404
|
+
handleSkillsPointActivityStats();
|
|
188405
|
+
handleActivitySkillsStats();
|
|
188406
|
+
handleCredentialsActivityStats();
|
|
188407
|
+
handleCoursesActivityStats();
|
|
188408
|
+
handleProgramsActivityStats();
|
|
188409
|
+
handlePathwaysActivityStats();
|
|
188410
|
+
handlePerLearnerInfoStats();
|
|
188411
|
+
}, []);
|
|
188412
|
+
return { stats };
|
|
188413
|
+
};
|
|
188414
|
+
|
|
188415
|
+
const EdxIframeContext = createContext({
|
|
188416
|
+
iframeUrl: '',
|
|
188417
|
+
setIframeUrl: () => { },
|
|
188418
|
+
courseOutline: {},
|
|
188419
|
+
setActiveTab: () => { },
|
|
188420
|
+
activeTab: '',
|
|
188421
|
+
courseID: '',
|
|
188422
|
+
currentlyInExamSubsection: false,
|
|
188423
|
+
setCurrentlyInExamSubsection: () => { },
|
|
188424
|
+
examInfo: null,
|
|
188425
|
+
setExamInfo: () => { },
|
|
188426
|
+
refresher: null,
|
|
188427
|
+
setRefresher: () => { },
|
|
188428
|
+
});
|
|
188429
|
+
|
|
188430
|
+
const CourseOutlineContext = createContext({
|
|
188431
|
+
courseOutline: {},
|
|
188432
|
+
courseOutlineLoading: false,
|
|
188433
|
+
expandedModule: '',
|
|
188434
|
+
expandedLessons: [],
|
|
188435
|
+
selectLesson: () => { },
|
|
188436
|
+
toggleModule: () => { },
|
|
188437
|
+
toggleLesson: () => { },
|
|
188438
|
+
currentChapter: '',
|
|
188439
|
+
currentLesson: '',
|
|
188440
|
+
course: null,
|
|
188441
|
+
courseOutlineDrawerOpen: false,
|
|
188442
|
+
setCourseOutlineDrawerOpen: () => { },
|
|
188443
|
+
currentUnitID: null,
|
|
188444
|
+
refetchCourseOutline: () => { },
|
|
188445
|
+
});
|
|
188446
|
+
|
|
188447
|
+
/**
|
|
188448
|
+
* Pure utility helpers for the edX iframe course navigation.
|
|
188449
|
+
*
|
|
188450
|
+
* Extracted from skillsai's `hooks/courses/use-edx-iframe.ts` so they can be
|
|
188451
|
+
* unit tested without a React renderer and reused from both the hook and
|
|
188452
|
+
* (future) course-content components. Any function that used to read
|
|
188453
|
+
* `config.urls.lms() / mfe() / legacyLmsUrl()` now takes those URLs as
|
|
188454
|
+
* parameters — there is no package-level config coupling here.
|
|
188455
|
+
*
|
|
188456
|
+
* The async `getIframeURL` wrapper (which calls `useLazyGetEdxSSOTokenQuery`)
|
|
188457
|
+
* remains inside the hook.
|
|
188458
|
+
*/
|
|
188459
|
+
/**
|
|
188460
|
+
* Walk the outline tree and return the id of the `sequential` parent block
|
|
188461
|
+
* whose children include the given vertical id, or null if not found.
|
|
188462
|
+
*/
|
|
188463
|
+
function findSequentialParent(data, verticalId) {
|
|
188464
|
+
if (!data)
|
|
188465
|
+
return null;
|
|
188466
|
+
if (data.type === 'sequential' && data.children) {
|
|
188467
|
+
for (const child of data.children) {
|
|
188468
|
+
if (child.id === verticalId)
|
|
188469
|
+
return data.id;
|
|
188470
|
+
const foundId = findSequentialParent(child, verticalId);
|
|
188471
|
+
if (foundId)
|
|
188472
|
+
return foundId;
|
|
188473
|
+
}
|
|
188474
|
+
}
|
|
188475
|
+
else if (data.children) {
|
|
188476
|
+
for (const child of data.children) {
|
|
188477
|
+
const foundId = findSequentialParent(child, verticalId);
|
|
188478
|
+
if (foundId)
|
|
188479
|
+
return foundId;
|
|
188480
|
+
}
|
|
188481
|
+
}
|
|
188482
|
+
return null;
|
|
188483
|
+
}
|
|
188484
|
+
/**
|
|
188485
|
+
* Recursively collect every `vertical`-type block in the tree into a flat
|
|
188486
|
+
* `{id, display_name}[]` list, preserving document order.
|
|
188487
|
+
*/
|
|
188488
|
+
function flattenVerticalBlocks(data) {
|
|
188489
|
+
if (!data || typeof data !== 'object')
|
|
188490
|
+
return [];
|
|
188491
|
+
if (Array.isArray(data)) {
|
|
188492
|
+
const result = [];
|
|
188493
|
+
for (const item of data) {
|
|
188494
|
+
result.push(...flattenVerticalBlocks(item));
|
|
188495
|
+
}
|
|
188496
|
+
return result;
|
|
188497
|
+
}
|
|
188498
|
+
if (data.type === 'vertical') {
|
|
188499
|
+
const block = {
|
|
188500
|
+
id: data.id,
|
|
188501
|
+
display_name: data.display_name,
|
|
188502
|
+
};
|
|
188503
|
+
return [block, ...flattenVerticalBlocks(data.children)];
|
|
188504
|
+
}
|
|
188505
|
+
return flattenVerticalBlocks(data.children);
|
|
188506
|
+
}
|
|
188507
|
+
/**
|
|
188508
|
+
* Walks `data.children[0].children[0].children[attempt]` up to `maxAttempts`
|
|
188509
|
+
* and returns the first non-empty unit. Falls back to the course's first
|
|
188510
|
+
* sub-sub block if the course has started. Returns null otherwise.
|
|
188511
|
+
*/
|
|
188512
|
+
function getFirstAvailableUnit(data, maxAttempts = 2) {
|
|
188513
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
188514
|
+
if (!data)
|
|
188515
|
+
return null;
|
|
188516
|
+
try {
|
|
188517
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
188518
|
+
const element = (_e = (_d = (_c = (_b = (_a = data.children) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.children) === null || _c === void 0 ? void 0 : _c[0]) === null || _d === void 0 ? void 0 : _d.children) === null || _e === void 0 ? void 0 : _e[attempt];
|
|
188519
|
+
if (element)
|
|
188520
|
+
return element;
|
|
188521
|
+
}
|
|
188522
|
+
}
|
|
188523
|
+
catch (_j) {
|
|
188524
|
+
// fallthrough to the tolerant branch below
|
|
188525
|
+
}
|
|
188526
|
+
try {
|
|
188527
|
+
if (data &&
|
|
188528
|
+
Object.prototype.hasOwnProperty.call(data, 'children') &&
|
|
188529
|
+
data.children &&
|
|
188530
|
+
data.children[0] &&
|
|
188531
|
+
Object.prototype.hasOwnProperty.call(data.children[0], 'children') &&
|
|
188532
|
+
(!data.start || new Date(data.start) < new Date())) {
|
|
188533
|
+
return (_h = (_g = (_f = data.children[0]) === null || _f === void 0 ? void 0 : _f.children) === null || _g === void 0 ? void 0 : _g[0]) !== null && _h !== void 0 ? _h : null;
|
|
188534
|
+
}
|
|
188535
|
+
}
|
|
188536
|
+
catch (_k) {
|
|
188537
|
+
return null;
|
|
188538
|
+
}
|
|
188539
|
+
return null;
|
|
188540
|
+
}
|
|
188541
|
+
/**
|
|
188542
|
+
* Find the deepest `resume_block: true` vertical node in the tree (last in
|
|
188543
|
+
* document order) or null if the course has never been started.
|
|
188544
|
+
*/
|
|
188545
|
+
function findLastResumeBlock(courseData) {
|
|
188546
|
+
if (!courseData)
|
|
188547
|
+
return null;
|
|
188548
|
+
let lastResumeBlock = null;
|
|
188549
|
+
const traverse = (node) => {
|
|
188550
|
+
if (node.resume_block && node.type === 'vertical') {
|
|
188551
|
+
lastResumeBlock = node;
|
|
188552
|
+
}
|
|
188553
|
+
if (node.children && node.children.length > 0) {
|
|
188554
|
+
for (const child of node.children)
|
|
188555
|
+
traverse(child);
|
|
188556
|
+
}
|
|
188557
|
+
};
|
|
188558
|
+
traverse(courseData);
|
|
188559
|
+
return lastResumeBlock;
|
|
188560
|
+
}
|
|
188561
|
+
/**
|
|
188562
|
+
* Find a specific block by id inside the top-level `children` array and
|
|
188563
|
+
* return it together with the chain of child-indices that locate it.
|
|
188564
|
+
*/
|
|
188565
|
+
function getParentBlockById(blocksArray, targetBlockId) {
|
|
188566
|
+
let foundIndices = [];
|
|
188567
|
+
const findParentBlock = (currentBlock, target, currentIndices) => {
|
|
188568
|
+
if (currentBlock.id === target) {
|
|
188569
|
+
foundIndices = currentIndices.slice();
|
|
188570
|
+
return currentBlock;
|
|
188571
|
+
}
|
|
188572
|
+
if (currentBlock.children) {
|
|
188573
|
+
for (let i = 0; i < currentBlock.children.length; i++) {
|
|
188574
|
+
const result = findParentBlock(currentBlock.children[i], target, [...currentIndices, i]);
|
|
188575
|
+
if (result)
|
|
188576
|
+
return result;
|
|
188577
|
+
}
|
|
188578
|
+
}
|
|
188579
|
+
return null;
|
|
188580
|
+
};
|
|
188581
|
+
for (let i = 0; i < blocksArray.length; i++) {
|
|
188582
|
+
const parentBlock = findParentBlock(blocksArray[i], targetBlockId, [i]);
|
|
188583
|
+
if (parentBlock)
|
|
188584
|
+
return { parentBlock, foundIndices };
|
|
188585
|
+
}
|
|
188586
|
+
return { parentBlock: null, foundIndices };
|
|
188587
|
+
}
|
|
188588
|
+
/**
|
|
188589
|
+
* Given a unit id and the course outline, return the id of the previous
|
|
188590
|
+
* vertical unit, or null if there is no previous unit.
|
|
188591
|
+
*/
|
|
188592
|
+
function getPreviousUnitIframe(suppliedId, courseData) {
|
|
188593
|
+
const idList = flattenVerticalBlocks(courseData);
|
|
188594
|
+
const index = idList.findIndex((item) => item.id === suppliedId);
|
|
188595
|
+
if (index === -1 || index === 0)
|
|
188596
|
+
return null;
|
|
188597
|
+
return idList[index - 1].id;
|
|
188598
|
+
}
|
|
188599
|
+
/**
|
|
188600
|
+
* Given a unit id and the course outline, return the id of the next vertical
|
|
188601
|
+
* unit, or null if there is no next unit.
|
|
188602
|
+
*/
|
|
188603
|
+
function getNextUnitIframe(suppliedId, courseData) {
|
|
188604
|
+
const idList = flattenVerticalBlocks(courseData);
|
|
188605
|
+
const index = idList.findIndex((item) => item.id === suppliedId);
|
|
188606
|
+
if (index === -1 || index === idList.length - 1)
|
|
188607
|
+
return null;
|
|
188608
|
+
return idList[index + 1].id;
|
|
188609
|
+
}
|
|
188610
|
+
/**
|
|
188611
|
+
* Given a sublesson (3rd-level) id, return the parent module + lesson objects
|
|
188612
|
+
* from a modules array. Returns `{module:{}, lesson:{}}` if not found — this
|
|
188613
|
+
* preserves the skillsai fallback shape so callers don't need to null-check.
|
|
188614
|
+
*/
|
|
188615
|
+
function getParentsInfosFromSublessonId(modules, sublessonId) {
|
|
188616
|
+
try {
|
|
188617
|
+
for (const mod of modules) {
|
|
188618
|
+
if (!mod.children)
|
|
188619
|
+
continue;
|
|
188620
|
+
for (const lesson of mod.children) {
|
|
188621
|
+
if (!lesson.children)
|
|
188622
|
+
continue;
|
|
188623
|
+
for (const sublesson of lesson.children) {
|
|
188624
|
+
if (sublesson.id === sublessonId) {
|
|
188625
|
+
return { module: mod, lesson };
|
|
188626
|
+
}
|
|
188627
|
+
}
|
|
188628
|
+
}
|
|
188629
|
+
}
|
|
188630
|
+
throw new Error('Sublesson not found');
|
|
188631
|
+
}
|
|
188632
|
+
catch (_a) {
|
|
188633
|
+
return { module: {}, lesson: {} };
|
|
188634
|
+
}
|
|
188635
|
+
}
|
|
188636
|
+
/**
|
|
188637
|
+
* Append a Bookmarks tab to the given tabs list in place.
|
|
188638
|
+
* The caller supplies the LMS base URL — no package config dependency.
|
|
188639
|
+
*/
|
|
188640
|
+
function addBookmarksTab(tabs, courseId, lmsUrl) {
|
|
188641
|
+
tabs.push({
|
|
188642
|
+
tab_id: 'bookmarks',
|
|
188643
|
+
title: 'Bookmarks',
|
|
188644
|
+
url: `${lmsUrl}/courses/${courseId}/bookmarks`,
|
|
188645
|
+
});
|
|
188646
|
+
}
|
|
188647
|
+
/**
|
|
188648
|
+
* Resolve which unit of the course outline should be loaded into the iframe.
|
|
188649
|
+
* 1. If the current URL has `?unit_id=…`, try to find that block.
|
|
188650
|
+
* 2. Otherwise, return the last resume block if the learner has progress.
|
|
188651
|
+
* 3. Otherwise, fall back to the first available unit.
|
|
188652
|
+
*
|
|
188653
|
+
* Priority order:
|
|
188654
|
+
* 1. Explicit `unitId` parameter (from React state — always up-to-date).
|
|
188655
|
+
* 2. `?unit_id=…` from the current URL (`window.location.href`).
|
|
188656
|
+
* 3. The last `resume_block` vertical in the outline.
|
|
188657
|
+
* 4. The first available unit.
|
|
188658
|
+
*/
|
|
188659
|
+
function getUnitToIframe(courseOutlineData, unitId) {
|
|
188660
|
+
var _a, _b;
|
|
188661
|
+
// 1. Explicit unitId from caller (e.g. React searchParams / context)
|
|
188662
|
+
if (unitId) {
|
|
188663
|
+
return (_a = findVerticalById(courseOutlineData, unitId)) !== null && _a !== void 0 ? _a : getFirstAvailableUnit(courseOutlineData);
|
|
188664
|
+
}
|
|
188665
|
+
// 2. Fall back to reading from the URL (legacy path / initial load)
|
|
188666
|
+
if (typeof window !== 'undefined') {
|
|
188667
|
+
try {
|
|
188668
|
+
const courseUrl = new URL(window.location.href);
|
|
188669
|
+
const urlUnitId = courseUrl.searchParams.get('unit_id');
|
|
188670
|
+
if (urlUnitId) {
|
|
188671
|
+
return ((_b = findVerticalById(courseOutlineData, urlUnitId)) !== null && _b !== void 0 ? _b : getFirstAvailableUnit(courseOutlineData));
|
|
188672
|
+
}
|
|
188673
|
+
}
|
|
188674
|
+
catch (_c) {
|
|
188675
|
+
// invalid URL — fall through
|
|
188676
|
+
}
|
|
188677
|
+
}
|
|
188678
|
+
// 3. Resume block
|
|
188679
|
+
const lastResumeBlock = findLastResumeBlock(courseOutlineData);
|
|
188680
|
+
if (lastResumeBlock)
|
|
188681
|
+
return lastResumeBlock;
|
|
188682
|
+
// 4. First available
|
|
188683
|
+
return getFirstAvailableUnit(courseOutlineData);
|
|
188684
|
+
}
|
|
188685
|
+
/** Find a block by id within the top-level `children` of a course outline. */
|
|
188686
|
+
function findVerticalById(data, verticalId) {
|
|
188687
|
+
if (!data || !data.children)
|
|
188688
|
+
return null;
|
|
188689
|
+
const search = (nodes) => {
|
|
188690
|
+
for (const item of nodes) {
|
|
188691
|
+
if (item.id === verticalId)
|
|
188692
|
+
return item;
|
|
188693
|
+
if (item.children) {
|
|
188694
|
+
const result = search(item.children);
|
|
188695
|
+
if (result)
|
|
188696
|
+
return result;
|
|
188697
|
+
}
|
|
188698
|
+
}
|
|
188699
|
+
return null;
|
|
188700
|
+
};
|
|
188701
|
+
return search(data.children);
|
|
188702
|
+
}
|
|
188703
|
+
|
|
188704
|
+
/**
|
|
188705
|
+
* Hook exposing helpers for the edX iframe course navigation. Ported from
|
|
188706
|
+
* skillsai's `hooks/courses/use-edx-iframe.ts`, with two changes:
|
|
188707
|
+
*
|
|
188708
|
+
* 1. All pure utilities are imported from `../lib/edx-iframe-utils`.
|
|
188709
|
+
* 2. LMS / MFE / legacy LMS base URLs are passed in as hook args instead of
|
|
188710
|
+
* being read from the skillsai `config` module.
|
|
188711
|
+
*/
|
|
188712
|
+
const useEdxIframe = ({ lmsUrl, mfeUrl, legacyLmsUrl }) => {
|
|
188713
|
+
const [getEdxSsoAuthToken] = useLazyGetEdxSSOTokenQuery();
|
|
188714
|
+
/**
|
|
188715
|
+
* Resolve the iframe URL for a given course + courseInfo value. The
|
|
188716
|
+
* `courseInfo` can be either the course outline object (pick a unit from
|
|
188717
|
+
* it) or an xblock id string (use as-is).
|
|
188718
|
+
*
|
|
188719
|
+
* `unitId` (optional) is the unit id from React state (e.g.
|
|
188720
|
+
* `searchParams.get('unit_id')` or `currentUnitID`). When provided it
|
|
188721
|
+
* takes precedence over reading `window.location.href`, which avoids a
|
|
188722
|
+
* race condition with Next.js async `router.push`.
|
|
188723
|
+
*/
|
|
188724
|
+
async function getIframeURL(courseId, courseInfo, callback, unitId) {
|
|
188725
|
+
var _a;
|
|
188726
|
+
if (typeof courseInfo === 'object' ||
|
|
188727
|
+
(typeof courseInfo === 'string' && courseInfo.includes(':'))) {
|
|
188728
|
+
// Course outline case — resolve an actual vertical to load.
|
|
188729
|
+
const outline = typeof courseInfo === 'object' ? courseInfo : null;
|
|
188730
|
+
if (outline) {
|
|
188731
|
+
flattenVerticalBlocks(outline);
|
|
188732
|
+
const unit = getUnitToIframe(outline, unitId);
|
|
188733
|
+
await addIframeUrl(courseId, (_a = unit === null || unit === void 0 ? void 0 : unit.id) !== null && _a !== void 0 ? _a : '', callback);
|
|
188734
|
+
return;
|
|
188735
|
+
}
|
|
188736
|
+
await addIframeUrl(courseId, courseInfo, callback);
|
|
188737
|
+
}
|
|
188738
|
+
else {
|
|
188739
|
+
await addIframeUrl(courseId, courseInfo, callback);
|
|
188740
|
+
}
|
|
188741
|
+
}
|
|
188742
|
+
async function addIframeUrl(courseId, rawXblockId, callback) {
|
|
188743
|
+
var _a, _b;
|
|
188744
|
+
let url = '';
|
|
188745
|
+
const xblockID = rawXblockId;
|
|
188746
|
+
let baseLMSIframeURL = `${lmsUrl}/xblock/${xblockID}?show_title=0&show_bookmark_button=1&recheck_access=1&view=student_view`;
|
|
188747
|
+
try {
|
|
188748
|
+
switch (xblockID) {
|
|
188749
|
+
case 'forum':
|
|
188750
|
+
url = `${mfeUrl}/discussions/${courseId}/posts`;
|
|
188751
|
+
break;
|
|
188752
|
+
case 'notes':
|
|
188753
|
+
url = `${mfeUrl}/courses/${courseId}/edxnotes`;
|
|
188754
|
+
break;
|
|
188755
|
+
case 'progress':
|
|
188756
|
+
url = `${mfeUrl}/learning/course/${courseId}/progress/`;
|
|
188757
|
+
break;
|
|
188758
|
+
case 'dates':
|
|
188759
|
+
url = `${mfeUrl}/learning/course/${courseId}/dates/`;
|
|
188760
|
+
break;
|
|
188761
|
+
case 'bookmarks':
|
|
188762
|
+
url = `${legacyLmsUrl}/courses/${courseId}/bookmarks/`;
|
|
188763
|
+
break;
|
|
188764
|
+
case 'instructor':
|
|
188765
|
+
baseLMSIframeURL = `${lmsUrl}/courses/${courseId}/instructor`;
|
|
188766
|
+
// fallthrough — same SSO-wrap path as default
|
|
188767
|
+
// eslint-disable-next-line no-fallthrough
|
|
188768
|
+
default: {
|
|
188769
|
+
const { data: authSsoToken } = await getEdxSsoAuthToken({
|
|
188770
|
+
username: (_a = getUserName()) !== null && _a !== void 0 ? _a : '',
|
|
188771
|
+
redirectUrl: baseLMSIframeURL,
|
|
188772
|
+
});
|
|
188773
|
+
url = `${legacyLmsUrl}/ibl/ai/sso/backend/edx/iframe?sso_auth_token=${(_b = authSsoToken === null || authSsoToken === void 0 ? void 0 : authSsoToken.sso_auth_token) !== null && _b !== void 0 ? _b : ''}`;
|
|
188774
|
+
break;
|
|
188775
|
+
}
|
|
188776
|
+
}
|
|
188777
|
+
callback(url);
|
|
188778
|
+
}
|
|
188779
|
+
catch (_c) {
|
|
188780
|
+
callback(url);
|
|
188781
|
+
}
|
|
188782
|
+
}
|
|
188783
|
+
return {
|
|
188784
|
+
getIframeURL,
|
|
188785
|
+
getUnitToIframe,
|
|
188786
|
+
findSequentialParent,
|
|
188787
|
+
flattenVerticalBlocks,
|
|
188788
|
+
getFirstAvailableUnit,
|
|
188789
|
+
findLastResumeBlock,
|
|
188790
|
+
getParentBlockById,
|
|
188791
|
+
getPreviousUnitIframe,
|
|
188792
|
+
getNextUnitIframe,
|
|
188793
|
+
addBookmarksTab,
|
|
188794
|
+
getParentsInfosFromSublessonId,
|
|
188795
|
+
};
|
|
188796
|
+
};
|
|
188797
|
+
|
|
188798
|
+
const ChatContext = createContext({
|
|
188799
|
+
isOpen: false,
|
|
188800
|
+
setIsOpen: () => { },
|
|
188801
|
+
courseMentor: null,
|
|
188802
|
+
setCourseMentor: () => { },
|
|
188803
|
+
mentorSidebarHidden: false,
|
|
188804
|
+
setMentorSidebarHidden: () => { },
|
|
188805
|
+
});
|
|
188806
|
+
const useChatState = () => useContext(ChatContext);
|
|
188807
|
+
function ChatProvider({ children, initialOpen = false, initialCourseMentor = null, initialMentorSidebarHidden = false, }) {
|
|
188808
|
+
const [isOpen, setIsOpen] = useState(initialOpen);
|
|
188809
|
+
const [courseMentor, setCourseMentor] = useState(initialCourseMentor);
|
|
188810
|
+
const [mentorSidebarHidden, setMentorSidebarHidden] = useState(initialMentorSidebarHidden);
|
|
188811
|
+
const value = {
|
|
188812
|
+
isOpen,
|
|
188813
|
+
setIsOpen,
|
|
188814
|
+
courseMentor,
|
|
188815
|
+
setCourseMentor,
|
|
188816
|
+
mentorSidebarHidden,
|
|
188817
|
+
setMentorSidebarHidden,
|
|
188818
|
+
};
|
|
188819
|
+
return createElement$1(ChatContext.Provider, { value }, children);
|
|
188820
|
+
}
|
|
188821
|
+
|
|
188822
|
+
/**
|
|
188823
|
+
* Ported from skillsai (`hooks/courses/use-course-detail.ts`).
|
|
188824
|
+
*
|
|
188825
|
+
* Differences from the skillsai version:
|
|
188826
|
+
* - No `next/navigation`. Caller passes an `onNavigate(href, { external })`
|
|
188827
|
+
* callback which is invoked for lesson open / access-course / Stripe
|
|
188828
|
+
* checkout redirect. `external: true` means "open in a new tab / navigate
|
|
188829
|
+
* the full window" (as opposed to SPA push).
|
|
188830
|
+
* - No internal `sonner` toasts. Caller may provide `onError` / `onSuccess`
|
|
188831
|
+
* callbacks (both optional).
|
|
188832
|
+
* - No `config.*` reads. Caller passes `dmUrl` (used to build the Stripe
|
|
188833
|
+
* `success_url`) and `courseEligibilityEnabled` (the feature flag that
|
|
188834
|
+
* used to live in `config.settings.courseEligibilityEnabled()`).
|
|
188835
|
+
*/
|
|
188836
|
+
const ACCESS_COURSE_LABEL = 'Access Course';
|
|
188837
|
+
const ENROLL_NOW_LABEL = 'Enroll Now';
|
|
188838
|
+
const REQUEST_ACCESS_LABEL = 'Request Access';
|
|
188839
|
+
const ENROLL_NOW_COURSE_STARTING_SOON_LABEL = 'Enroll Now - Course Starting Soon';
|
|
188840
|
+
const REQUEST_ACCESS_COURSE_STARTING_SOON_LABEL = 'Request Access - Course Starting Soon';
|
|
188841
|
+
const INVITATION_ONLY_LABEL = 'Invitation Only';
|
|
188842
|
+
const BUY_NOW_LABEL = 'Buy Now';
|
|
188843
|
+
const useCourseDetail = ({ courseId, dmUrl, courseEligibilityEnabled = false, onNavigate, onError, onSuccess, }) => {
|
|
188844
|
+
const [createCourseEnrollment, { isError: isCourseEnrollmentError }] = useCreateCourseEnrollmentMutation();
|
|
188845
|
+
const [createStripeCheckoutSession] = useCreateStripeCheckoutSessionMutation();
|
|
188846
|
+
const [getCourseProgress, { isLoading: isCourseProgressLoading, isError: isCourseProgressError },] = useLazyGetCourseProgressQuery();
|
|
188847
|
+
const [getCourseCompletion, { isLoading: isCourseCompletionLoading, isError: isCourseCompletionError },] = useLazyGetCourseCompletionQuery();
|
|
188848
|
+
const { handleFetchCourseMetaData, handleFetchCourseCompletionOutlines, handleFetchCourseEligibility, } = useCourseMetadata();
|
|
188849
|
+
const [courseInfoLoadingState, setCourseInfoLoadingState] = useState('not-started');
|
|
188850
|
+
const [course, setCourse] = useState(null);
|
|
188851
|
+
const [courseOutline, setCourseOutline] = useState({});
|
|
188852
|
+
const [courseOutlineLoading, setCourseOutlineLoading] = useState(false);
|
|
188853
|
+
const [courseEligibilityLoading, setCourseEligibilityLoading] = useState(false);
|
|
188854
|
+
const [courseButtonActionLoading, setCourseButtonActionLoading] = useState(false);
|
|
188855
|
+
const [courseProgress, setCourseProgress] = useState(null);
|
|
188856
|
+
const [courseCompletion, setCourseCompletion] = useState(null);
|
|
188857
|
+
const [courseGradingPolicyActive, setCourseGradingPolicyActive] = useState(false);
|
|
188858
|
+
const navigateToCourseContent = (href) => {
|
|
188859
|
+
if (onNavigate) {
|
|
188860
|
+
onNavigate(href, { external: inIframe() });
|
|
188861
|
+
}
|
|
188862
|
+
};
|
|
188863
|
+
const handleRequestAccess = () => {
|
|
188864
|
+
// Intentional no-op — skillsai's original also had no behavior here.
|
|
188865
|
+
// Callers that need request-access wiring should listen to btn_label.
|
|
188866
|
+
};
|
|
188867
|
+
const handleSelfEnrollToCourse = () => {
|
|
188868
|
+
// Same as skillsai — placeholder. Self-enroll is distinguished from
|
|
188869
|
+
// normal enroll only by the label; callers can key off it if needed.
|
|
188870
|
+
};
|
|
188871
|
+
const handleAccessCourse = () => {
|
|
188872
|
+
navigateToCourseContent(`/course-content/${courseId}/course`);
|
|
188873
|
+
};
|
|
188874
|
+
const handleCreateCheckoutSession = async () => {
|
|
188875
|
+
setCourseButtonActionLoading(true);
|
|
188876
|
+
const currentTenant = getTenant();
|
|
188877
|
+
try {
|
|
188878
|
+
const checkoutSession = await createStripeCheckoutSession({
|
|
188879
|
+
sku: courseId,
|
|
188880
|
+
org: (course === null || course === void 0 ? void 0 : course.org) || '',
|
|
188881
|
+
tenant: (course === null || course === void 0 ? void 0 : course.platform_key) || currentTenant,
|
|
188882
|
+
username: getUserName() || '',
|
|
188883
|
+
mode: 'payment',
|
|
188884
|
+
cancel_url: typeof window !== 'undefined' ? window.location.href : '',
|
|
188885
|
+
success_url: `${dmUrl}/api/service/orgs/${course === null || course === void 0 ? void 0 : course.platform_key}/stripe/course-payment-callback/`,
|
|
188886
|
+
}).unwrap();
|
|
188887
|
+
if (!(checkoutSession === null || checkoutSession === void 0 ? void 0 : checkoutSession.redirect_to)) {
|
|
188888
|
+
throw new Error('Failed to create checkout session');
|
|
188889
|
+
}
|
|
188890
|
+
onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess('Redirecting to checkout page...');
|
|
188891
|
+
onNavigate === null || onNavigate === void 0 ? void 0 : onNavigate(checkoutSession.redirect_to, { external: true });
|
|
188892
|
+
}
|
|
188893
|
+
catch (_a) {
|
|
188894
|
+
setCourseButtonActionLoading(false);
|
|
188895
|
+
onError === null || onError === void 0 ? void 0 : onError('Failed to create checkout session');
|
|
188896
|
+
}
|
|
188897
|
+
};
|
|
188898
|
+
const handleEnrollToCourse = async () => {
|
|
188899
|
+
setCourseButtonActionLoading(true);
|
|
188900
|
+
try {
|
|
188901
|
+
const response = await createCourseEnrollment({
|
|
188902
|
+
course_details: { course_id: courseId },
|
|
188903
|
+
});
|
|
188904
|
+
if (isCourseEnrollmentError || !response.data || !response.data.created) {
|
|
188905
|
+
throw new Error('Failed to enroll in course');
|
|
188906
|
+
}
|
|
188907
|
+
onSuccess === null || onSuccess === void 0 ? void 0 : onSuccess('Enrolled in course successfully');
|
|
188908
|
+
setCourseButtonActionLoading(false);
|
|
188909
|
+
handleAccessCourse();
|
|
188910
|
+
}
|
|
188911
|
+
catch (_a) {
|
|
188912
|
+
onError === null || onError === void 0 ? void 0 : onError('Failed to enroll in course.');
|
|
188913
|
+
setCourseButtonActionLoading(false);
|
|
188914
|
+
}
|
|
188915
|
+
};
|
|
188916
|
+
const [courseEligibility, setCourseEligibility] = useState({
|
|
188917
|
+
btn_label: ENROLL_NOW_LABEL,
|
|
188918
|
+
btn_action: handleEnrollToCourse,
|
|
188919
|
+
});
|
|
188920
|
+
const handleFetchCourseEligibilityInfo = async () => {
|
|
188921
|
+
setCourseEligibilityLoading(true);
|
|
188922
|
+
const eligibility = await handleFetchCourseEligibility(courseId);
|
|
188923
|
+
if (eligibility) {
|
|
188924
|
+
const enrollmentStarted = dayjs(course === null || course === void 0 ? void 0 : course.enrollment_start).diff(dayjs(), 'seconds') > 0;
|
|
188925
|
+
const isEnrolled = eligibility.is_enrolled;
|
|
188926
|
+
const canEnroll = eligibility.can_enroll;
|
|
188927
|
+
const isEligible = eligibility.is_eligible;
|
|
188928
|
+
const isNotMainTenant = getTenant() !== 'main';
|
|
188929
|
+
const invitationOnly = eligibility.invitation_only;
|
|
188930
|
+
const coursePrice = course === null || course === void 0 ? void 0 : course.course_price;
|
|
188931
|
+
if (courseEligibilityEnabled) {
|
|
188932
|
+
if (isNotMainTenant && isEnrolled && enrollmentStarted && isEligible) {
|
|
188933
|
+
setCourseEligibility({
|
|
188934
|
+
btn_label: ACCESS_COURSE_LABEL,
|
|
188935
|
+
btn_action: handleAccessCourse,
|
|
188936
|
+
});
|
|
188937
|
+
}
|
|
188938
|
+
else if (isNotMainTenant && enrollmentStarted && !isEligible) {
|
|
188939
|
+
setCourseEligibility({
|
|
188940
|
+
btn_label: REQUEST_ACCESS_LABEL,
|
|
188941
|
+
btn_action: handleRequestAccess,
|
|
188942
|
+
});
|
|
188943
|
+
}
|
|
188944
|
+
else if (!isNotMainTenant &&
|
|
188945
|
+
!enrollmentStarted &&
|
|
188946
|
+
isEligible &&
|
|
188947
|
+
canEnroll &&
|
|
188948
|
+
!isEnrolled) {
|
|
188949
|
+
setCourseEligibility({
|
|
188950
|
+
btn_label: ENROLL_NOW_COURSE_STARTING_SOON_LABEL,
|
|
188951
|
+
btn_action: handleSelfEnrollToCourse,
|
|
188952
|
+
});
|
|
188953
|
+
}
|
|
188954
|
+
else if (!isNotMainTenant &&
|
|
188955
|
+
!enrollmentStarted &&
|
|
188956
|
+
!isEligible &&
|
|
188957
|
+
(course === null || course === void 0 ? void 0 : course.platform_key) === 'main') {
|
|
188958
|
+
setCourseEligibility({
|
|
188959
|
+
btn_label: REQUEST_ACCESS_COURSE_STARTING_SOON_LABEL,
|
|
188960
|
+
btn_action: handleRequestAccess,
|
|
188961
|
+
});
|
|
188962
|
+
}
|
|
188963
|
+
else if (isNotMainTenant && enrollmentStarted && isEligible && !isEnrolled && canEnroll) {
|
|
188964
|
+
setCourseEligibility({
|
|
188965
|
+
btn_label: ENROLL_NOW_LABEL,
|
|
188966
|
+
btn_action: handleSelfEnrollToCourse,
|
|
188967
|
+
});
|
|
188968
|
+
}
|
|
188969
|
+
}
|
|
188970
|
+
else {
|
|
188971
|
+
if (isEnrolled) {
|
|
188972
|
+
setCourseEligibility({
|
|
188973
|
+
btn_label: ACCESS_COURSE_LABEL,
|
|
188974
|
+
btn_action: handleAccessCourse,
|
|
188975
|
+
});
|
|
188976
|
+
}
|
|
188977
|
+
else if (invitationOnly) {
|
|
188978
|
+
setCourseEligibility({
|
|
188979
|
+
disabled: true,
|
|
188980
|
+
btn_label: INVITATION_ONLY_LABEL,
|
|
188981
|
+
btn_action: () => { },
|
|
188982
|
+
});
|
|
188983
|
+
}
|
|
188984
|
+
else if (coursePrice && coursePrice !== 'Free' && parseInt(coursePrice) !== 0) {
|
|
188985
|
+
setCourseEligibility({
|
|
188986
|
+
btn_label: BUY_NOW_LABEL,
|
|
188987
|
+
btn_action: handleCreateCheckoutSession,
|
|
188988
|
+
});
|
|
188989
|
+
}
|
|
188990
|
+
else {
|
|
188991
|
+
setCourseEligibility({
|
|
188992
|
+
btn_label: ENROLL_NOW_LABEL,
|
|
188993
|
+
btn_action: handleEnrollToCourse,
|
|
188994
|
+
});
|
|
188995
|
+
}
|
|
188996
|
+
}
|
|
188997
|
+
setCourseEligibilityLoading(false);
|
|
188998
|
+
}
|
|
188999
|
+
else {
|
|
189000
|
+
setCourseEligibility({
|
|
189001
|
+
btn_label: ENROLL_NOW_LABEL,
|
|
189002
|
+
btn_action: handleEnrollToCourse,
|
|
189003
|
+
});
|
|
189004
|
+
setCourseEligibilityLoading(false);
|
|
189005
|
+
}
|
|
189006
|
+
};
|
|
189007
|
+
const handleFetchCourseInfo = async () => {
|
|
189008
|
+
setCourseInfoLoadingState('loading');
|
|
189009
|
+
try {
|
|
189010
|
+
const courseMetaData = await handleFetchCourseMetaData(courseId);
|
|
189011
|
+
if (courseMetaData && Object.keys(courseMetaData).length > 0) {
|
|
189012
|
+
setCourse(courseMetaData);
|
|
189013
|
+
setCourseInfoLoadingState('successful');
|
|
189014
|
+
}
|
|
189015
|
+
else {
|
|
189016
|
+
setCourse(null);
|
|
189017
|
+
setCourseInfoLoadingState('failure');
|
|
189018
|
+
}
|
|
189019
|
+
}
|
|
189020
|
+
catch (_a) {
|
|
189021
|
+
setCourse(null);
|
|
189022
|
+
setCourseInfoLoadingState('failure');
|
|
189023
|
+
}
|
|
189024
|
+
};
|
|
189025
|
+
const handleFetchCourseSyllabus = async (setLoadingState = true) => {
|
|
189026
|
+
if (setLoadingState) {
|
|
189027
|
+
setCourseOutlineLoading(true);
|
|
189028
|
+
}
|
|
189029
|
+
const outlines = (await handleFetchCourseCompletionOutlines(courseId));
|
|
189030
|
+
if (outlines && Object.keys(outlines).length > 0) {
|
|
189031
|
+
setCourseOutline(outlines);
|
|
189032
|
+
}
|
|
189033
|
+
else {
|
|
189034
|
+
setCourseOutline({});
|
|
189035
|
+
}
|
|
189036
|
+
if (setLoadingState) {
|
|
189037
|
+
setCourseOutlineLoading(false);
|
|
189038
|
+
}
|
|
189039
|
+
};
|
|
189040
|
+
const handleOpenLesson = (lessonId, checkEligibility = false) => {
|
|
189041
|
+
if (lessonId &&
|
|
189042
|
+
(checkEligibility ? courseEligibility.btn_label === ACCESS_COURSE_LABEL : true)) {
|
|
189043
|
+
navigateToCourseContent(`/course-content/${courseId}/course?unit_id=${encodeURIComponent(lessonId)}`);
|
|
189044
|
+
}
|
|
189045
|
+
};
|
|
189046
|
+
const handleFetchCourseProgress = async () => {
|
|
189047
|
+
var _a;
|
|
189048
|
+
try {
|
|
189049
|
+
const progress = await getCourseProgress({ courseKey: courseId });
|
|
189050
|
+
if (isCourseProgressError) {
|
|
189051
|
+
throw new Error('Error fetching course progress');
|
|
189052
|
+
}
|
|
189053
|
+
setCourseProgress(progress.data || null);
|
|
189054
|
+
if (progress.data) {
|
|
189055
|
+
setCourseGradingPolicyActive(Array.isArray((_a = progress.data.grading_policy) === null || _a === void 0 ? void 0 : _a.assignment_policies) &&
|
|
189056
|
+
progress.data.grading_policy.assignment_policies.length > 0);
|
|
189057
|
+
}
|
|
189058
|
+
}
|
|
189059
|
+
catch (error) {
|
|
189060
|
+
setCourseProgress(null);
|
|
189061
|
+
console.error('Error fetching course progress:', error);
|
|
189062
|
+
}
|
|
189063
|
+
};
|
|
189064
|
+
const handleFetchCourseCompletion = async (userID) => {
|
|
189065
|
+
try {
|
|
189066
|
+
const completion = await getCourseCompletion({
|
|
189067
|
+
courseKey: encodeURIComponent(courseId),
|
|
189068
|
+
userID,
|
|
189069
|
+
});
|
|
189070
|
+
if (isCourseCompletionError) {
|
|
189071
|
+
throw new Error('Error fetching course completion');
|
|
189072
|
+
}
|
|
189073
|
+
setCourseCompletion(completion.data || null);
|
|
189074
|
+
}
|
|
189075
|
+
catch (error) {
|
|
189076
|
+
setCourseCompletion(null);
|
|
189077
|
+
console.error('Error fetching course completion:', error);
|
|
189078
|
+
}
|
|
189079
|
+
};
|
|
189080
|
+
return {
|
|
189081
|
+
// handlers
|
|
189082
|
+
handleRequestAccess,
|
|
189083
|
+
handleSelfEnrollToCourse,
|
|
189084
|
+
handleAccessCourse,
|
|
189085
|
+
handleCreateCheckoutSession,
|
|
189086
|
+
handleEnrollToCourse,
|
|
189087
|
+
handleFetchCourseEligibilityInfo,
|
|
189088
|
+
handleFetchCourseInfo,
|
|
189089
|
+
handleFetchCourseSyllabus,
|
|
189090
|
+
handleOpenLesson,
|
|
189091
|
+
handleFetchCourseProgress,
|
|
189092
|
+
handleFetchCourseCompletion,
|
|
189093
|
+
// state
|
|
189094
|
+
course,
|
|
189095
|
+
courseInfoLoadingState,
|
|
189096
|
+
courseOutline,
|
|
189097
|
+
courseEligibility,
|
|
189098
|
+
courseOutlineLoading,
|
|
189099
|
+
courseEligibilityLoading,
|
|
189100
|
+
courseButtonActionLoading,
|
|
189101
|
+
isCourseProgressLoading,
|
|
189102
|
+
isCourseCompletionLoading,
|
|
189103
|
+
courseProgress,
|
|
189104
|
+
courseCompletion,
|
|
189105
|
+
courseGradingPolicyActive,
|
|
189106
|
+
// labels (re-exported here for convenience; also exported as module
|
|
189107
|
+
// constants at the top of this file)
|
|
189108
|
+
ACCESS_COURSE_LABEL,
|
|
189109
|
+
ENROLL_NOW_LABEL,
|
|
189110
|
+
REQUEST_ACCESS_LABEL,
|
|
189111
|
+
ENROLL_NOW_COURSE_STARTING_SOON_LABEL,
|
|
189112
|
+
REQUEST_ACCESS_COURSE_STARTING_SOON_LABEL,
|
|
189113
|
+
INVITATION_ONLY_LABEL,
|
|
189114
|
+
BUY_NOW_LABEL,
|
|
189115
|
+
};
|
|
189116
|
+
};
|
|
189117
|
+
|
|
189118
|
+
/**
|
|
189119
|
+
* Hierarchical course outline (chapters → lessons → sublessons) with
|
|
189120
|
+
* completion progress indicators and collapsible tree UI.
|
|
189121
|
+
*
|
|
189122
|
+
* Ported from skillsai (`components/course-outline.tsx`). Reads all state
|
|
189123
|
+
* from `CourseOutlineContext` — callers provide it by wrapping this
|
|
189124
|
+
* component in a `<CourseOutlineContext.Provider value={...} />` (or by
|
|
189125
|
+
* using the provided `CourseContentLayout`, which sets up the provider).
|
|
189126
|
+
*/
|
|
189127
|
+
const MAX_CHECKMARK_POINT = 7;
|
|
189128
|
+
const getCompletionRatio = (node) => {
|
|
189129
|
+
if (!Array.isArray(node.children) || node.children.length === 0) {
|
|
189130
|
+
return node.complete ? 1 : 0;
|
|
189131
|
+
}
|
|
189132
|
+
const totalChildren = node.children.length;
|
|
189133
|
+
const completedScore = node.children.reduce((acc, child) => acc + getCompletionRatio(child), 0);
|
|
189134
|
+
return completedScore / totalChildren;
|
|
189135
|
+
};
|
|
189136
|
+
const getCompletionLevel = (node) => {
|
|
189137
|
+
return Math.round(getCompletionRatio(node) * MAX_CHECKMARK_POINT);
|
|
189138
|
+
};
|
|
189139
|
+
const CompletionIcon = ({ node }) => {
|
|
189140
|
+
const level = getCompletionLevel(node);
|
|
189141
|
+
const size = 16;
|
|
189142
|
+
const strokeWidth = 2;
|
|
189143
|
+
const radius = (size - strokeWidth) / 2;
|
|
189144
|
+
const circumference = 2 * Math.PI * radius;
|
|
189145
|
+
const progress = level / MAX_CHECKMARK_POINT;
|
|
189146
|
+
const dashOffset = circumference * (1 - progress);
|
|
189147
|
+
if (level === MAX_CHECKMARK_POINT) {
|
|
189148
|
+
return (jsxs("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, className: "flex-shrink-0", children: [jsx("circle", { cx: size / 2, cy: size / 2, r: radius, fill: "#f59e0b", stroke: "none" }), jsx("path", { d: "M5 8.5L7 10.5L11 6", fill: "none", stroke: "white", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" })] }));
|
|
189149
|
+
}
|
|
189150
|
+
if (level === 0) {
|
|
189151
|
+
return (jsx("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, className: "flex-shrink-0", children: jsx("circle", { cx: size / 2, cy: size / 2, r: radius, fill: "none", stroke: "#d1d5db", strokeWidth: strokeWidth }) }));
|
|
189152
|
+
}
|
|
189153
|
+
return (jsxs("svg", { width: size, height: size, viewBox: `0 0 ${size} ${size}`, className: "flex-shrink-0", children: [jsx("circle", { cx: size / 2, cy: size / 2, r: radius, fill: "none", stroke: "#e5e7eb", strokeWidth: strokeWidth }), jsx("circle", { cx: size / 2, cy: size / 2, r: radius, fill: "none", stroke: "#f59e0b", strokeWidth: strokeWidth, strokeDasharray: circumference, strokeDashoffset: dashOffset, strokeLinecap: "round", transform: `rotate(-90 ${size / 2} ${size / 2})` })] }));
|
|
189154
|
+
};
|
|
189155
|
+
const SkeletonRow = () => jsx("div", { className: "mb-2 h-6 w-full animate-pulse rounded bg-gray-200" });
|
|
189156
|
+
const CourseOutline = () => {
|
|
189157
|
+
const { courseOutline, courseOutlineLoading, expandedModule, expandedLessons, selectLesson, toggleModule, toggleLesson, currentChapter, currentLesson, } = useContext(CourseOutlineContext);
|
|
189158
|
+
return (jsx("div", { className: "h-full overflow-y-auto md:h-[calc(100%-35px)]", style: { scrollbarWidth: 'none' }, children: courseOutlineLoading ? (jsx("div", { className: "p-3", children: Array.from({ length: 8 }, (_v, i) => (jsx(SkeletonRow, {}, i))) })) : (Array.isArray(courseOutline === null || courseOutline === void 0 ? void 0 : courseOutline.children) &&
|
|
189159
|
+
courseOutline.children.map((module) => (jsxs("div", { className: "border-b border-gray-200", children: [jsxs("button", { type: "button", onClick: () => toggleModule(module.id), className: `flex w-full items-center justify-between p-3 text-left hover:bg-gray-50 ${expandedModule === module.id ? 'bg-gray-50' : ''}`, children: [jsx("span", { className: "text-sm font-medium text-gray-700", children: module.display_name }), jsx(ChevronRight, { className: `h-4 w-4 text-gray-500 transition-transform ${expandedModule === module.id ? 'rotate-90 transform' : ''}` })] }), expandedModule === module.id && module.children && (jsx("div", { className: "pr-2 pb-2 pl-6", children: module.children.map((lesson) => (jsxs("div", { children: [jsxs("button", { type: "button", onClick: () => toggleLesson(lesson.id), className: `mb-1 flex w-full items-center justify-between rounded-sm p-2 text-left text-sm ${currentChapter === lesson.id
|
|
189160
|
+
? 'bg-amber-50 text-amber-700'
|
|
189161
|
+
: 'text-gray-600 hover:bg-gray-50'}`, children: [jsxs("div", { className: "flex items-center", children: [jsx("div", { className: "mr-2 flex-shrink-0", children: jsx(CompletionIcon, { node: lesson }) }), jsx("span", { children: lesson.display_name })] }), lesson.children && lesson.children.length > 0 && (jsx(ChevronRight, { className: `h-4 w-4 text-gray-500 transition-transform ${expandedLessons.includes(lesson.id) ? 'rotate-90 transform' : ''}` }))] }), lesson.children &&
|
|
189162
|
+
lesson.children.length > 0 &&
|
|
189163
|
+
expandedLessons.includes(lesson.id) && (jsx("div", { className: "pr-2 pb-2 pl-6", children: lesson.children.map((sublesson) => (jsxs("button", { type: "button", onClick: () => selectLesson(sublesson.id), className: `mb-1 flex w-full items-center rounded-sm p-2 text-left text-sm ${currentLesson === sublesson.id
|
|
189164
|
+
? 'bg-amber-50 text-amber-700'
|
|
189165
|
+
: 'text-gray-600 hover:bg-gray-50'}`, children: [jsx("div", { className: "mr-2 flex-shrink-0", children: jsx(CompletionIcon, { node: sublesson }) }), jsx("span", { children: sublesson.display_name })] }, sublesson.id))) }))] }, lesson.id))) }))] }, module.id)))) }));
|
|
189166
|
+
};
|
|
189167
|
+
|
|
189168
|
+
function Sheet({ ...props }) {
|
|
189169
|
+
return jsx(Root$b, { "data-slot": "sheet", ...props });
|
|
189170
|
+
}
|
|
189171
|
+
function SheetPortal({ ...props }) {
|
|
189172
|
+
return jsx(Portal$6, { "data-slot": "sheet-portal", ...props });
|
|
189173
|
+
}
|
|
189174
|
+
function SheetOverlay({ className, ...props }) {
|
|
189175
|
+
return (jsx(Overlay$1, { "data-slot": "sheet-overlay", className: cn('data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50', className), ...props }));
|
|
189176
|
+
}
|
|
189177
|
+
function SheetContent({ className, children, side = 'right', ...props }) {
|
|
189178
|
+
return (jsxs(SheetPortal, { children: [jsx(SheetOverlay, {}), jsxs(Content$3, { "data-slot": "sheet-content", className: cn('bg-background data-[state=open]:animate-in data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out data-[state=closed]:duration-300 data-[state=open]:duration-500', side === 'right' &&
|
|
189179
|
+
'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm', side === 'left' &&
|
|
189180
|
+
'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm', side === 'top' &&
|
|
189181
|
+
'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b', side === 'bottom' &&
|
|
189182
|
+
'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t', className), ...props, children: [children, jsxs(Close$1, { className: "ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none", children: [jsx(X, { className: "size-4" }), jsx("span", { className: "sr-only", children: "Close" })] })] })] }));
|
|
189183
|
+
}
|
|
189184
|
+
function SheetHeader({ className, ...props }) {
|
|
189185
|
+
return (jsx("div", { "data-slot": "sheet-header", className: cn('flex flex-col gap-1.5 p-4', className), ...props }));
|
|
189186
|
+
}
|
|
189187
|
+
function SheetTitle({ className, ...props }) {
|
|
189188
|
+
return (jsx(Title$1, { "data-slot": "sheet-title", className: cn('text-foreground font-semibold', className), ...props }));
|
|
189189
|
+
}
|
|
189190
|
+
|
|
189191
|
+
/**
|
|
189192
|
+
* Mobile drawer wrapping `<CourseOutline />` in a `Sheet` for responsive
|
|
189193
|
+
* course navigation. Ported from skillsai's `course-outline-drawer.tsx`.
|
|
189194
|
+
* All state comes from `CourseOutlineContext`.
|
|
189195
|
+
*/
|
|
189196
|
+
const CourseOutlineDrawer = () => {
|
|
189197
|
+
const { course, courseOutlineDrawerOpen, setCourseOutlineDrawerOpen } = useContext(CourseOutlineContext);
|
|
189198
|
+
return (jsx(Sheet, { open: courseOutlineDrawerOpen, onOpenChange: setCourseOutlineDrawerOpen, children: jsxs(SheetContent, { side: "left", className: "flex w-72 flex-col p-0", children: [jsx(SheetHeader, { className: "border-b border-gray-200 p-4", children: jsx(SheetTitle, { className: "text-left font-semibold text-gray-800", children: course === null || course === void 0 ? void 0 : course.display_name }) }), jsx(CourseOutline, {})] }) }));
|
|
189199
|
+
};
|
|
189200
|
+
|
|
189201
|
+
/**
|
|
189202
|
+
* Timed exam overlay + countdown. Ported from skillsai
|
|
189203
|
+
* (`components/edx-iframe/timed-exam.tsx`). Reads all state from
|
|
189204
|
+
* `EdxIframeContext`; no internal toasts or routing.
|
|
189205
|
+
*
|
|
189206
|
+
* Errors from the RTK exam mutations are caught and logged via
|
|
189207
|
+
* `console.error` — same behavior as the skillsai original. Callers that
|
|
189208
|
+
* want user-facing error toasts can wrap the data-layer calls themselves
|
|
189209
|
+
* higher up, or intercept via a custom RTK middleware.
|
|
189210
|
+
*/
|
|
189211
|
+
const TimedExam = () => {
|
|
189212
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
|
|
189213
|
+
const { examInfo, setExamInfo, setRefresher } = useContext(EdxIframeContext);
|
|
189214
|
+
const [isReadyToStart, setIsReadyToStart] = useState(false);
|
|
189215
|
+
const [timeRemaining, setTimeRemaining] = useState(0);
|
|
189216
|
+
const [showFullInstructions, setShowFullInstructions] = useState(false);
|
|
189217
|
+
const [showEndExamModal, setShowEndExamModal] = useState(false);
|
|
189218
|
+
const [updateExamAttempt, { isLoading: isSubmittingExam }] = useUpdateExamAttemptMutation();
|
|
189219
|
+
const [startExam, { isLoading: isStartingExam }] = useStartExamMutation();
|
|
189220
|
+
const [getExamInfo] = useLazyGetExamInfoQuery();
|
|
189221
|
+
// Initialize timer when exam is started
|
|
189222
|
+
useEffect(() => {
|
|
189223
|
+
var _a, _b, _c, _d;
|
|
189224
|
+
if (((_b = (_a = examInfo === null || examInfo === void 0 ? void 0 : examInfo.exam) === null || _a === void 0 ? void 0 : _a.attempt) === null || _b === void 0 ? void 0 : _b.attempt_status) === 'started' &&
|
|
189225
|
+
((_d = (_c = examInfo === null || examInfo === void 0 ? void 0 : examInfo.exam) === null || _c === void 0 ? void 0 : _c.attempt) === null || _d === void 0 ? void 0 : _d.time_remaining_seconds)) {
|
|
189226
|
+
setTimeRemaining(Math.floor(examInfo.exam.attempt.time_remaining_seconds));
|
|
189227
|
+
}
|
|
189228
|
+
}, [examInfo]);
|
|
189229
|
+
const updateExamInfo = async () => {
|
|
189230
|
+
if (!examInfo)
|
|
189231
|
+
return;
|
|
189232
|
+
const updated = await getExamInfo({
|
|
189233
|
+
course_id: examInfo.exam.course_id,
|
|
189234
|
+
content_id: examInfo.exam.content_id,
|
|
189235
|
+
is_learning_mfe: true,
|
|
189236
|
+
}, false);
|
|
189237
|
+
setExamInfo((updated === null || updated === void 0 ? void 0 : updated.data) || null);
|
|
189238
|
+
};
|
|
189239
|
+
// Countdown timer
|
|
189240
|
+
useEffect(() => {
|
|
189241
|
+
var _a, _b;
|
|
189242
|
+
if (timeRemaining > 0 && ((_b = (_a = examInfo === null || examInfo === void 0 ? void 0 : examInfo.exam) === null || _a === void 0 ? void 0 : _a.attempt) === null || _b === void 0 ? void 0 : _b.attempt_status) === 'started') {
|
|
189243
|
+
const timer = setInterval(() => {
|
|
189244
|
+
setTimeRemaining((prev) => {
|
|
189245
|
+
var _a;
|
|
189246
|
+
const newTime = prev > 0 ? prev - 1 : 0;
|
|
189247
|
+
if (newTime === 0 && ((_a = examInfo === null || examInfo === void 0 ? void 0 : examInfo.exam) === null || _a === void 0 ? void 0 : _a.id)) {
|
|
189248
|
+
updateExamAttempt({
|
|
189249
|
+
attemptID: examInfo.exam.attempt.attempt_id,
|
|
189250
|
+
action: 'submit',
|
|
189251
|
+
})
|
|
189252
|
+
.unwrap()
|
|
189253
|
+
.then(() => {
|
|
189254
|
+
updateExamInfo();
|
|
189255
|
+
setRefresher(new Date());
|
|
189256
|
+
})
|
|
189257
|
+
.catch((error) => {
|
|
189258
|
+
console.error('Failed to auto-submit exam:', error);
|
|
189259
|
+
});
|
|
189260
|
+
}
|
|
189261
|
+
return newTime;
|
|
189262
|
+
});
|
|
189263
|
+
}, 1000);
|
|
189264
|
+
return () => clearInterval(timer);
|
|
189265
|
+
}
|
|
189266
|
+
return undefined;
|
|
189267
|
+
}, [timeRemaining, (_b = (_a = examInfo === null || examInfo === void 0 ? void 0 : examInfo.exam) === null || _a === void 0 ? void 0 : _a.attempt) === null || _b === void 0 ? void 0 : _b.attempt_status, (_d = (_c = examInfo === null || examInfo === void 0 ? void 0 : examInfo.exam) === null || _c === void 0 ? void 0 : _c.attempt) === null || _d === void 0 ? void 0 : _d.attempt_id]);
|
|
189268
|
+
if (!examInfo) {
|
|
189269
|
+
return null;
|
|
189270
|
+
}
|
|
189271
|
+
const formatTimeRemaining = (seconds) => {
|
|
189272
|
+
const hours = Math.floor(seconds / 3600);
|
|
189273
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
189274
|
+
const secs = seconds % 60;
|
|
189275
|
+
if (hours > 0) {
|
|
189276
|
+
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
|
189277
|
+
}
|
|
189278
|
+
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
|
189279
|
+
};
|
|
189280
|
+
const { exam } = examInfo;
|
|
189281
|
+
const timeLimitHours = Math.floor(exam.time_limit_mins / 60);
|
|
189282
|
+
const timeLimitMinutes = exam.time_limit_mins % 60;
|
|
189283
|
+
const formatTimeLimit = () => {
|
|
189284
|
+
if (timeLimitHours > 0 && timeLimitMinutes > 0) {
|
|
189285
|
+
return `${timeLimitHours} hour${timeLimitHours > 1 ? 's' : ''} ${timeLimitMinutes} minute${timeLimitMinutes > 1 ? 's' : ''}`;
|
|
189286
|
+
}
|
|
189287
|
+
else if (timeLimitHours > 0) {
|
|
189288
|
+
return `${timeLimitHours} hour${timeLimitHours > 1 ? 's' : ''}`;
|
|
189289
|
+
}
|
|
189290
|
+
return `${timeLimitMinutes} minute${timeLimitMinutes > 1 ? 's' : ''}`;
|
|
189291
|
+
};
|
|
189292
|
+
const handleStartExam = async () => {
|
|
189293
|
+
var _a;
|
|
189294
|
+
try {
|
|
189295
|
+
setIsReadyToStart(true);
|
|
189296
|
+
if ((_a = examInfo === null || examInfo === void 0 ? void 0 : examInfo.exam) === null || _a === void 0 ? void 0 : _a.id) {
|
|
189297
|
+
const formData = new FormData();
|
|
189298
|
+
formData.append('exam_id', examInfo.exam.id.toString());
|
|
189299
|
+
formData.append('start_clock', 'true');
|
|
189300
|
+
await startExam(formData).unwrap();
|
|
189301
|
+
await updateExamInfo();
|
|
189302
|
+
}
|
|
189303
|
+
}
|
|
189304
|
+
catch (error) {
|
|
189305
|
+
console.error('Failed to start exam:', error);
|
|
189306
|
+
setIsReadyToStart(false);
|
|
189307
|
+
}
|
|
189308
|
+
};
|
|
189309
|
+
const handleEndExam = () => {
|
|
189310
|
+
setShowEndExamModal(true);
|
|
189311
|
+
};
|
|
189312
|
+
const handleConfirmEndExam = async () => {
|
|
189313
|
+
var _a;
|
|
189314
|
+
try {
|
|
189315
|
+
if ((_a = examInfo === null || examInfo === void 0 ? void 0 : examInfo.exam) === null || _a === void 0 ? void 0 : _a.id) {
|
|
189316
|
+
await updateExamAttempt({
|
|
189317
|
+
attemptID: examInfo.exam.attempt.attempt_id,
|
|
189318
|
+
action: 'submit',
|
|
189319
|
+
}).unwrap();
|
|
189320
|
+
setShowEndExamModal(false);
|
|
189321
|
+
await updateExamInfo();
|
|
189322
|
+
setRefresher(new Date());
|
|
189323
|
+
}
|
|
189324
|
+
}
|
|
189325
|
+
catch (error) {
|
|
189326
|
+
console.error('Failed to submit exam:', error);
|
|
189327
|
+
}
|
|
189328
|
+
};
|
|
189329
|
+
const handleCancelEndExam = () => {
|
|
189330
|
+
setShowEndExamModal(false);
|
|
189331
|
+
};
|
|
189332
|
+
if (((_f = (_e = examInfo === null || examInfo === void 0 ? void 0 : examInfo.exam) === null || _e === void 0 ? void 0 : _e.attempt) === null || _f === void 0 ? void 0 : _f.attempt_status) === 'started') {
|
|
189333
|
+
const isLowTime = timeRemaining <= (examInfo.exam.attempt.low_threshold_sec || 14400);
|
|
189334
|
+
const isCriticalTime = timeRemaining <= (examInfo.exam.attempt.critically_low_threshold_sec || 3600);
|
|
189335
|
+
return (jsxs("div", { className: "mx-auto max-w-4xl px-6 pt-6", children: [jsxs("div", { className: `rounded-lg border p-4 ${isCriticalTime
|
|
189336
|
+
? 'border-red-200 bg-red-50'
|
|
189337
|
+
: isLowTime
|
|
189338
|
+
? 'border-yellow-200 bg-yellow-50'
|
|
189339
|
+
: 'border-blue-200 bg-blue-50'}`, children: [jsx("div", { className: "mb-4 text-sm text-gray-600", children: showFullInstructions ? (jsxs(Fragment$1, { children: ["You are taking \"", examInfo.exam.exam_name, "\" as a timed exam. The timer below shows the time remaining in the exam. To receive credit for problems, you must select \"Submit\" for each problem before you select \"End My Exam\".", ' ', jsx("button", { type: "button", className: "text-blue-600 underline hover:text-blue-800", onClick: () => setShowFullInstructions(false), children: "Show less" })] })) : (jsxs(Fragment$1, { children: ["You are taking \"", examInfo.exam.exam_name, "\" as a timed exam.", ' ', jsx("button", { type: "button", className: "text-blue-600 underline hover:text-blue-800", onClick: () => setShowFullInstructions(true), children: "Show more" })] })) }), jsxs("div", { className: "flex items-center gap-4", children: [jsxs("div", { className: "flex items-center gap-2", children: [jsx(Clock, { className: `h-5 w-5 ${isCriticalTime ? 'text-red-600' : isLowTime ? 'text-yellow-600' : 'text-blue-600'}` }), jsx("span", { className: `text-md font-mono font-semibold ${isCriticalTime ? 'text-red-700' : isLowTime ? 'text-yellow-700' : 'text-blue-700'}`, "aria-live": "polite", "aria-label": `Time remaining: ${formatTimeRemaining(timeRemaining)}`, children: formatTimeRemaining(timeRemaining) })] }), jsx("button", { type: "button", onClick: handleEndExam, className: "rounded-md border border-gray-300 bg-white px-4 py-2 font-medium text-gray-700 transition-colors hover:bg-gray-50 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none", "aria-describedby": "end-exam-warning", children: "End My Exam" }), jsx("div", { id: "end-exam-warning", className: "sr-only", children: "Warning: Ending the exam will submit all your answers and you cannot return to continue." })] })] }), showEndExamModal && (jsx("div", { className: "fixed inset-0 z-50 flex items-center justify-center bg-black/50", children: jsxs("div", { className: "mx-4 w-full max-w-full rounded-lg bg-white p-6 shadow-xl md:max-w-[40%]", children: [jsx("h2", { className: "mb-4 text-lg font-semibold text-gray-900", children: "Are you sure that you want to submit your timed exam?" }), jsx("p", { className: "mb-3 text-gray-700", children: "Make sure that you have selected \"Submit\" for each problem before you submit your exam." }), jsx("p", { className: "mb-6 text-gray-700", children: "After you submit your exam, your exam will be graded." }), jsxs("div", { className: "flex gap-3", children: [jsx("button", { type: "button", onClick: handleConfirmEndExam, disabled: isSubmittingExam, className: "rounded bg-blue-600 px-4 py-2 font-medium text-white transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50", children: isSubmittingExam ? 'Submitting...' : 'Yes, submit my timed exam.' }), jsx("button", { type: "button", onClick: handleCancelEndExam, className: "rounded px-4 py-2 font-medium text-blue-600 transition-colors hover:bg-blue-50 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none", children: "No, I want to continue working." })] })] }) }))] }));
|
|
189340
|
+
}
|
|
189341
|
+
if (((_h = (_g = examInfo === null || examInfo === void 0 ? void 0 : examInfo.exam) === null || _g === void 0 ? void 0 : _g.attempt) === null || _h === void 0 ? void 0 : _h.attempt_status) === 'submitted') {
|
|
189342
|
+
return null;
|
|
189343
|
+
}
|
|
189344
|
+
const noActiveAttempt = !((_j = examInfo === null || examInfo === void 0 ? void 0 : examInfo.exam) === null || _j === void 0 ? void 0 : _j.attempt) || Object.keys(examInfo.exam.attempt).length === 0;
|
|
189345
|
+
const noActiveAttemptTop = !(examInfo === null || examInfo === void 0 ? void 0 : examInfo.active_attempt) || Object.keys(examInfo.active_attempt).length === 0;
|
|
189346
|
+
if (noActiveAttempt || noActiveAttemptTop) {
|
|
189347
|
+
return (jsxs("div", { className: "sm:p-6", children: [jsx("div", { className: "mb-8 rounded-lg border border-blue-200 bg-blue-50 p-6", children: jsxs("div", { className: "flex items-start gap-3", children: [jsx(Clock, { className: "mt-1 h-6 w-6 flex-shrink-0 text-blue-600" }), jsxs("div", { className: "flex-1", children: [jsxs("h2", { className: "mb-3 text-xl font-semibold text-gray-900", children: [examInfo.exam.exam_name, " is a Timed Exam (", formatTimeLimit(), ")"] }), jsxs("p", { className: "mb-4 leading-relaxed text-gray-700", children: ["This exam has a time limit associated with it. To pass this exam, you must complete the problems in the time allowed. After you select I am ready to start this timed exam, you will have ", formatTimeLimit(), " to complete and submit the exam."] }), jsxs("button", { type: "button", onClick: handleStartExam, disabled: isStartingExam || isReadyToStart, className: "inline-flex items-center gap-2 rounded-md bg-blue-600 px-6 py-3 font-medium text-white transition-colors hover:bg-blue-700 focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50", "aria-describedby": "exam-time-info", children: [jsx(Clock, { className: "hidden h-4 w-4 md:block" }), isStartingExam ? 'Starting exam...' : 'I am ready to start this timed exam.'] }), jsxs("div", { id: "exam-time-info", className: "sr-only", children: ["Starting this exam will begin a ", formatTimeLimit(), " timer that cannot be paused."] })] })] }) }), jsxs("div", { className: "mb-8", children: [jsx("h3", { className: "mb-4 text-lg font-medium text-gray-900", children: "Can I request additional time to complete my exam?" }), jsxs("div", { className: "flex items-start gap-3", children: [jsx(CircleAlert, { className: "mt-0.5 h-5 w-5 flex-shrink-0 text-gray-500" }), jsx("p", { className: "leading-relaxed text-gray-700", children: "If you have disabilities, you might be eligible for an additional time allowance on timed exams. Ask your course team for information about additional time allowances." })] })] })] }));
|
|
189348
|
+
}
|
|
189349
|
+
return null;
|
|
189350
|
+
};
|
|
189351
|
+
|
|
189352
|
+
/**
|
|
189353
|
+
* Prop-driven port of skillsai's `CourseAccessGuard`. Responsibilities
|
|
189354
|
+
* unchanged: show a spinner while the course is loading, surface not-found
|
|
189355
|
+
* / unauthorized states via callbacks, render children only on a clean
|
|
189356
|
+
* authorized load. Routing is now the caller's responsibility.
|
|
189357
|
+
*
|
|
189358
|
+
* Callers should stabilize `onUnauthorized` / `onNotFound` with `useCallback`
|
|
189359
|
+
* (or hoist them to module scope) to avoid spurious re-fires when the effect
|
|
189360
|
+
* re-runs.
|
|
189361
|
+
*/
|
|
189362
|
+
const CourseAccessGuard = ({ course, courseInfoLoadingState, currentTenant, onUnauthorized, onNotFound, children, }) => {
|
|
189363
|
+
const isLoaded = courseInfoLoadingState === 'successful' || courseInfoLoadingState === 'failure';
|
|
189364
|
+
const isUnauthorizedTenant = isLoaded &&
|
|
189365
|
+
!!(course === null || course === void 0 ? void 0 : course.platform_key) &&
|
|
189366
|
+
course.platform_key !== currentTenant &&
|
|
189367
|
+
course.platform_key !== 'main';
|
|
189368
|
+
const isNotFound = courseInfoLoadingState === 'failure' && !course;
|
|
189369
|
+
useEffect(() => {
|
|
189370
|
+
if (isUnauthorizedTenant) {
|
|
189371
|
+
onUnauthorized === null || onUnauthorized === void 0 ? void 0 : onUnauthorized();
|
|
189372
|
+
}
|
|
189373
|
+
else if (isNotFound) {
|
|
189374
|
+
onNotFound === null || onNotFound === void 0 ? void 0 : onNotFound();
|
|
189375
|
+
}
|
|
189376
|
+
}, [isUnauthorizedTenant, isNotFound, onUnauthorized, onNotFound]);
|
|
189377
|
+
if (!isLoaded || isUnauthorizedTenant || isNotFound) {
|
|
189378
|
+
return (jsx("div", { className: "flex flex-1 items-center justify-center", children: jsx("div", { className: "h-8 w-8 animate-spin rounded-full border-4 border-amber-500 border-t-transparent" }) }));
|
|
189379
|
+
}
|
|
189380
|
+
return jsx(Fragment$1, { children: children });
|
|
189381
|
+
};
|
|
189382
|
+
|
|
189383
|
+
/**
|
|
189384
|
+
* Loading skeleton shown while `CourseContentLayout` is hydrating.
|
|
189385
|
+
* Ported from skillsai (`app/course-content/[course_id]/loading.tsx`).
|
|
189386
|
+
*/
|
|
189387
|
+
const CourseContentLoading = () => {
|
|
189388
|
+
return (jsxs("div", { className: "flex min-h-screen flex-col", children: [jsx("div", { className: "h-16 flex-shrink-0 border-b border-gray-200 bg-white md:h-20" }), jsxs("div", { className: "flex flex-1 flex-col md:flex-row", children: [jsxs("div", { className: "w-full border-r border-gray-200 md:w-72", children: [jsx("div", { className: "border-b border-gray-200 p-4", children: jsx(Skeleton, { className: "h-6 w-48" }) }), jsx("div", { className: "space-y-4 p-4", children: Array.from({ length: 6 }, (_v, i) => (jsxs("div", { className: "space-y-2", children: [jsx(Skeleton, { className: "h-5 w-full" }), i === 0 && (jsxs("div", { className: "space-y-2 pl-4", children: [jsx(Skeleton, { className: "h-4 w-5/6" }), jsx(Skeleton, { className: "h-4 w-4/6" })] }))] }, i))) })] }), jsxs("div", { className: "flex flex-1 flex-col", children: [jsxs("div", { className: "border-b border-gray-200", children: [jsx("div", { className: "flex p-4", children: Array.from({ length: 5 }, (_v, i) => (jsx(Skeleton, { className: "mx-2 h-4 w-16" }, i))) }), jsxs("div", { className: "flex items-center justify-between bg-gray-50 p-4", children: [jsxs("div", { className: "flex items-center", children: [jsx(Skeleton, { className: "mr-2 h-3 w-24" }), jsx(Skeleton, { className: "mx-1 h-3 w-3" }), jsx(Skeleton, { className: "mr-2 h-3 w-32" }), jsx(Skeleton, { className: "mx-1 h-3 w-3" }), jsx(Skeleton, { className: "h-3 w-16" })] }), jsxs("div", { className: "flex items-center", children: [jsx(Skeleton, { className: "mr-4 h-3 w-16" }), jsx(Skeleton, { className: "h-3 w-16" })] })] })] }), jsx("div", { className: "h-full bg-amber-50 p-6", children: jsxs("div", { className: "mx-auto max-w-4xl", children: [jsx(Skeleton, { className: "mb-4 h-8 w-64" }), jsx(Skeleton, { className: "mb-2 h-4 w-full" }), jsx(Skeleton, { className: "mb-6 h-4 w-5/6" }), jsx("div", { className: "mb-6 aspect-video rounded-md bg-gray-200" }), jsxs("div", { className: "mb-6 rounded-md border border-gray-200 bg-white p-4", children: [jsx(Skeleton, { className: "mb-2 h-5 w-40" }), jsx(Skeleton, { className: "mb-2 h-4 w-full" }), jsx(Skeleton, { className: "mb-2 h-4 w-full" }), jsx(Skeleton, { className: "h-4 w-3/4" })] }), jsxs("div", { className: "flex items-center justify-between", children: [jsx(Skeleton, { className: "h-10 w-32" }), jsx(Skeleton, { className: "h-10 w-32" })] })] }) })] })] })] }));
|
|
189389
|
+
};
|
|
189390
|
+
|
|
186826
189391
|
/**
|
|
186827
189392
|
* LoginButton Component
|
|
186828
189393
|
*
|
|
@@ -188480,5 +191045,5 @@ var event = /*#__PURE__*/Object.freeze({
|
|
|
188480
191045
|
listen: listen
|
|
188481
191046
|
});
|
|
188482
191047
|
|
|
188483
|
-
export { AccessTimeHeatmap, AlertsTab, AnalyticsCourseDetail, AnalyticsCourses, AnalyticsFinancialStats, AnalyticsLayout, AnalyticsMonetizationStats, AnalyticsOverview, AnalyticsProgramDetail, AnalyticsPrograms, AnalyticsReportDownload, AnalyticsReports, AnalyticsSettingsProvider, AnalyticsTopicsStats, AnalyticsTranscriptsStats, AnalyticsUsersStats, ChartCardWrapper, ChartFiltersProvider, CompanyDialog, ConnectorManagementDialog, CopyButtonIcon, CreateWorkflowModal, CustomDateRangePicker, DeleteWorkflowModal, EditAlertDialog, EducationDialog, EducationTab, EmptyStats, ExperienceDialog, ExperienceTab, GroupsFilterDropdown, InstitutionDialog, InviteUserDialog, InvitedUsersDialog, Loader, LocalLLMTab, LoginButton, Markdown, NotificationDisplay, NotificationDropdown, PaywallModal, Profile, ResumeTab, RichTextEditor, SearchableMultiSelect, SendNotificationDialog, SignupButton, Spinner, StatCard, TAURI_COMMANDS, TAURI_EVENTS, TenantSwitcher, TimeFilter, TimeTrackingProvider, ToolDialogs, TopBanner, Version, WorkflowSidebar, initialModelDownloadState, isLocalLLMEnabled, isTauriApp, components as markdownComponents, sanitizeCss, setLocalLLMEnabled, useAnalyticsSettings, useChartFilters, useCourses, useFinancial, useIframeMessageHandler, useLocalStorage, useModelDownload, useMonetization, useOverview, usePrograms, useReports, useTauri, useTimeTracking, useTopics, useTranscripts, useUsers };
|
|
191048
|
+
export { ACCESS_COURSE_LABEL, AccessTimeHeatmap, AddSkillDialog, AlertsTab, AnalyticsCourseDetail, AnalyticsCourses, AnalyticsFinancialStats, AnalyticsLayout, AnalyticsMonetizationStats, AnalyticsOverview, AnalyticsProgramDetail, AnalyticsPrograms, AnalyticsReportDownload, AnalyticsReports, AnalyticsSettingsProvider, AnalyticsTopicsStats, AnalyticsTranscriptsStats, AnalyticsUsersStats, BUY_NOW_LABEL, ChartCardWrapper, ChartFiltersProvider, ChatContext, ChatProvider, CompanyDialog, ConnectorManagementDialog, CopyButtonIcon, CourseAccessGuard, CourseCardSkeleton, CourseContentLoading, CourseOutline, CourseOutlineContext, CourseOutlineDrawer, CreateWorkflowModal, CredentialBox, CredentialMiniBoxSkeleton, CustomDateRangePicker, CustomTooltip, DefaultEmptyBox, DeleteWorkflowModal, ENROLL_NOW_COURSE_STARTING_SOON_LABEL, ENROLL_NOW_LABEL, EditAlertDialog, EducationBox, EducationDialog, EducationTab, EdxIframeContext, EmptyStats, ExperienceBox, ExperienceDialog, ExperienceTab, GroupsFilterDropdown, INVITATION_ONLY_LABEL, InstitutionDialog, InviteUserDialog, InvitedUsersDialog, Loader, LocalLLMTab, LoginButton, Markdown, NotificationDisplay, NotificationDropdown, PaywallModal, Profile, ProfileTimeChart, REQUEST_ACCESS_COURSE_STARTING_SOON_LABEL, REQUEST_ACCESS_LABEL, ResumeBox, ResumeTab, RichTextEditor, SearchableMultiSelect, SendNotificationDialog, SignupButton, SkeletonActivityStatBox, SkeletonAddSkillsLoading, SkeletonCreatePathwaySearchList, SkeletonEducationBox, SkeletonMultiplier, SkeletonPathwayBox, SkeletonProfileInfoCard, SkeletonSkillBox, SkillBox, SkillDetailModal, SkillLeaderboardChart, SkillsBox, Spinner, StatCard, TAURI_COMMANDS, TAURI_EVENTS, TenantSwitcher, TimeFilter, TimeTrackingProvider, TimedExam, ToolDialogs, TopBanner, UserAvatar, Version, WorkflowSidebar, addBookmarksTab, findLastResumeBlock, findSequentialParent, flattenVerticalBlocks, getFirstAvailableUnit, getNextUnitIframe, getOrg, getParentBlockById, getParentsInfosFromSublessonId, getPreviousUnitIframe, getRandomCourseImage, getTenant, getUnitToIframe, getUserId, getUserName, inBrowserPrint, inIframe, initialModelDownloadState, isLocalLLMEnabled, isTauriApp, components as markdownComponents, sanitizeCss, setLocalLLMEnabled, useAnalyticsSettings, useCatalogSearch, useChartFilters, useChatState, useCourseDetail, useCourseMetadata, useCourses, useEdxIframe, useFinancial, useIframeMessageHandler, useLocalStorage, useModelDownload, useMonetization, useOverview, useProfileActivityStats, useProfileCredentials, useProfilePathways, useProfilePrograms, useProfileSkills, useProfileTimeSpent, usePrograms, useReports, useTauri, useTimeTracking, useTopics, useTranscripts, useUserCourses, useUserMetadata, useUsers };
|
|
188484
191049
|
//# sourceMappingURL=index.esm.js.map
|