@iblai/iblai-js 2.18.0 → 2.18.1

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.
@@ -189484,7 +189484,7 @@ function RetrievedDocumentsButton({ sessionId = '', documents, className, label,
189484
189484
  // Safe inside clickable transcript rows: the row must not react.
189485
189485
  event.stopPropagation();
189486
189486
  setIsOpen(true);
189487
- }, "data-testid": "retrieved-documents-button", children: [jsx(FileText, { className: "h-4 w-4 text-blue-600" }), jsx("span", { children: label !== null && label !== void 0 ? label : t('retrievedDocumentsButton.title') })] }), jsx(Dialog, { open: isOpen, onOpenChange: setIsOpen, children: jsxs(DialogContent, { className: "flex max-h-[80vh] flex-col gap-0 p-0 sm:max-w-[425px] md:max-w-[600px]", children: [jsx(DialogHeader, { className: "border-b px-4 py-2", children: jsxs(DialogTitle, { className: "flex items-center gap-2 text-base font-medium text-[#646464]", children: [jsx(FileText, { className: "h-5 w-5 text-blue-600" }), t('retrievedDocumentsButton.title')] }) }), jsx("div", { className: "min-h-0 flex-1 overflow-y-auto", "data-testid": "retrieved-documents-scroll", children: jsx(DocumentSidebar, { isModal: true, sessionId: sessionId, documents: documents }) })] }) })] }));
189487
+ }, "data-testid": "retrieved-documents-button", children: [jsx(FileText, { className: "h-4 w-4 text-blue-600" }), jsx("span", { children: label !== null && label !== void 0 ? label : t('retrievedDocumentsButton.title') })] }), jsx(Dialog, { open: isOpen, onOpenChange: setIsOpen, children: jsxs(DialogContent, { className: "flex max-h-[80vh] flex-col gap-0 p-0 sm:max-w-[425px] md:max-w-[600px]", children: [jsx(DialogHeader, { className: "border-b border-gray-200 px-4 py-2", children: jsxs(DialogTitle, { className: "flex items-center gap-2 text-base font-medium text-[#646464]", children: [jsx(FileText, { className: "h-5 w-5 text-blue-600" }), t('retrievedDocumentsButton.title')] }) }), jsx("div", { className: "min-h-0 flex-1 overflow-y-auto", "data-testid": "retrieved-documents-scroll", children: jsx(DocumentSidebar, { isModal: true, sessionId: sessionId, documents: documents }) })] }) })] }));
189488
189488
  }
189489
189489
 
189490
189490
  var COLLAPSIBLE_NAME = "Collapsible";
@@ -232420,6 +232420,154 @@ const useFinancial = ({ tenantKey, mentorId, userId, }) => {
232420
232420
  };
232421
232421
  };
232422
232422
 
232423
+ /**
232424
+ * One place that decides how a user is shown.
232425
+ *
232426
+ * Backend payloads name their user in a dozen ways — `email` / `lti_email` /
232427
+ * `user_email`, `username` / `student` / `lti_username`, `name` /
232428
+ * `full_name` / `user_full_name`, or a nested `user` object — and most of
232429
+ * them can be empty, `null`, or a placeholder like "None". Every surface used
232430
+ * to pick through those on its own. `resolveUserIdentity()` takes whatever
232431
+ * the payload is and returns what to show, what to open a profile on, and
232432
+ * the avatar initial, so the rule lives here and can be tweaked once.
232433
+ *
232434
+ * Default rule: a real full name first (see {@link hasDistinctName}), else
232435
+ * email → username → the anonymous label.
232436
+ */
232437
+ const DEFAULT_USER_IDENTITY_ORDER = ['email', 'name', 'username'];
232438
+ /** Order used when the payload carries a real name (see {@link hasDistinctName}). */
232439
+ const NAME_FIRST_ORDER = ['name', 'email', 'username'];
232440
+ /**
232441
+ * Lower-case letters and digits of any script, accents and punctuation dropped,
232442
+ * so "Conrad Mugabe" / "conrad.mugabe" and "José" / "jose" compare equal.
232443
+ */
232444
+ const identityKey = (value) => (value !== null && value !== void 0 ? value : '')
232445
+ .normalize('NFKD')
232446
+ .replace(/[^\p{L}\p{N}]/gu, '')
232447
+ .toLowerCase();
232448
+ /**
232449
+ * Whether `name` is a real name rather than a restatement of the username.
232450
+ *
232451
+ * Many payloads fill the name field with the username ("conradmugabe" →
232452
+ * "Conrad Mugabe", "fayyaz_4_ibleducation_com"), which tells an admin nothing
232453
+ * the email does not. A name that matches none of the usernames on the record
232454
+ * is the most recognisable thing on it — including for LTI learners, whose
232455
+ * minted `<opaque-id>@edx-lti-1p3.com` address and username identify nobody.
232456
+ */
232457
+ function hasDistinctName(name, ...usernames) {
232458
+ const key = identityKey(name);
232459
+ if (!key)
232460
+ return false;
232461
+ return usernames.every((username) => key !== identityKey(username));
232462
+ }
232463
+ const DEFAULT_ANONYMOUS_LABEL = 'Anonymous';
232464
+ /** Values backends emit instead of "nothing". Compared case-insensitively. */
232465
+ const PLACEHOLDERS = new Set([
232466
+ '',
232467
+ 'none',
232468
+ 'null',
232469
+ 'undefined',
232470
+ 'n/a',
232471
+ 'na',
232472
+ '-',
232473
+ 'anonymous',
232474
+ 'anonymoususer',
232475
+ ]);
232476
+ /**
232477
+ * `local@domain.tld` with no whitespace, checked with a single pass rather than
232478
+ * a backtracking pattern (CodeQL flags `[^@]+@[^@]+\.[^@]+` as polynomial on
232479
+ * adversarial input).
232480
+ */
232481
+ const isEmailLike = (value) => {
232482
+ const text = value.trim();
232483
+ if (!text || /\s/.test(text))
232484
+ return false;
232485
+ const at = text.indexOf('@');
232486
+ if (at <= 0 || at !== text.lastIndexOf('@') || at === text.length - 1)
232487
+ return false;
232488
+ const domain = text.slice(at + 1);
232489
+ const dot = domain.indexOf('.');
232490
+ // A dot somewhere inside the domain, with something after the last one.
232491
+ return dot > 0 && !domain.endsWith('.');
232492
+ };
232493
+ /** Trimmed string, or undefined for blanks, non-strings and placeholders. */
232494
+ const clean = (value) => {
232495
+ if (typeof value !== 'string')
232496
+ return undefined;
232497
+ const trimmed = value.trim();
232498
+ return PLACEHOLDERS.has(trimmed.toLowerCase()) ? undefined : trimmed;
232499
+ };
232500
+ const firstOf = (...candidates) => {
232501
+ for (const candidate of candidates) {
232502
+ const value = clean(candidate);
232503
+ if (value)
232504
+ return value;
232505
+ }
232506
+ return undefined;
232507
+ };
232508
+ const nested = (source) => source.user && typeof source.user === 'object' ? source.user : undefined;
232509
+ /**
232510
+ * Resolve how a user should be displayed from any payload that names one.
232511
+ *
232512
+ * - `email`: `lti_email`, `email`, `user_email`, the nested `user`'s email —
232513
+ * or a username field that turns out to hold an email address.
232514
+ * - `username`: `lti_username`, `username`, `student`, `user_username`, the
232515
+ * nested `user`'s username, or a bare string/number `user` value.
232516
+ * - `name`: `user_full_name`, `full_name`, `name`, `first_name last_name`, or
232517
+ * the nested `user`'s name.
232518
+ * - `profileUsername`: the first *platform* username (never an LTI claim).
232519
+ *
232520
+ * A string source is treated as an email when it looks like one, else as a
232521
+ * username. Placeholder strings ("None", "-", "anonymous"…) count as absent.
232522
+ *
232523
+ * The default order flips to name → email → username when the record carries a
232524
+ * real name (see {@link hasDistinctName}): "Joseph Oti" is more use to an admin
232525
+ * than his address, while a name that merely repeats the username is not, so
232526
+ * those records keep showing the email. An explicit `order` is always obeyed.
232527
+ */
232528
+ function resolveUserIdentity(source, options = {}) {
232529
+ var _a, _b;
232530
+ const anonymousLabel = options.anonymousLabel || DEFAULT_ANONYMOUS_LABEL;
232531
+ const order = ((_a = options.order) === null || _a === void 0 ? void 0 : _a.length) ? options.order : DEFAULT_USER_IDENTITY_ORDER;
232532
+ const record = source && typeof source === 'object' ? source : { user: source !== null && source !== void 0 ? source : null };
232533
+ const inner = nested(record);
232534
+ // A bare `user` value is an id or username; numeric ids are still shown.
232535
+ const bareUser = typeof record.user === 'number' && Number.isFinite(record.user)
232536
+ ? String(record.user)
232537
+ : typeof record.user === 'string'
232538
+ ? clean(record.user)
232539
+ : undefined;
232540
+ let email = firstOf(record.lti_email, record.email, record.user_email, inner === null || inner === void 0 ? void 0 : inner.lti_email, inner === null || inner === void 0 ? void 0 : inner.email, inner === null || inner === void 0 ? void 0 : inner.user_email);
232541
+ const platformUsername = firstOf(record.username, record.student, record.user_username, inner === null || inner === void 0 ? void 0 : inner.username, inner === null || inner === void 0 ? void 0 : inner.student, inner === null || inner === void 0 ? void 0 : inner.user_username, bareUser && !isEmailLike(bareUser) ? bareUser : undefined);
232542
+ let username = firstOf(record.lti_username, inner === null || inner === void 0 ? void 0 : inner.lti_username, platformUsername);
232543
+ const composed = [clean(record.first_name), clean(record.last_name)].filter(Boolean).join(' ');
232544
+ const name = firstOf(record.user_full_name, record.full_name, record.name, composed, inner === null || inner === void 0 ? void 0 : inner.user_full_name, inner === null || inner === void 0 ? void 0 : inner.full_name, inner === null || inner === void 0 ? void 0 : inner.name);
232545
+ // A bare string that looks like an email is an email, and so is a
232546
+ // "username" field that actually carries one.
232547
+ if (!email && bareUser && isEmailLike(bareUser))
232548
+ email = bareUser;
232549
+ if (!email && username && isEmailLike(username)) {
232550
+ email = username;
232551
+ username = undefined;
232552
+ }
232553
+ const facts = { email, username, name };
232554
+ // Every username on the record counts: the platform one a profile opens on
232555
+ // and the LTI claim that would be displayed.
232556
+ const labelOrder = !((_b = options.order) === null || _b === void 0 ? void 0 : _b.length) && hasDistinctName(name, platformUsername, username)
232557
+ ? NAME_FIRST_ORDER
232558
+ : order;
232559
+ const label = labelOrder.map((field) => facts[field]).find(Boolean);
232560
+ return {
232561
+ label: label !== null && label !== void 0 ? label : anonymousLabel,
232562
+ email,
232563
+ username,
232564
+ name,
232565
+ profileUsername: platformUsername,
232566
+ initial: (label !== null && label !== void 0 ? label : anonymousLabel).charAt(0).toUpperCase() || 'A',
232567
+ isAnonymous: !label,
232568
+ };
232569
+ }
232570
+
232423
232571
  const TONES = {
232424
232572
  amber: {
232425
232573
  container: 'border-amber-200 bg-amber-50 text-amber-800',
@@ -232777,6 +232925,10 @@ const costTick = (value) => {
232777
232925
  function SpendTab({ tenantKey, currentSPA, mentorId: currentMentorId, userId, onOpenUserTraces, }) {
232778
232926
  var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s;
232779
232927
  const t = useT();
232928
+ // The label a cost row shows for its user, by the shared identity rule.
232929
+ const identify = React__default.useCallback((row) => resolveUserIdentity(row, {
232930
+ anonymousLabel: t('analyticsAnalyticsFinancialStats.anonymous'),
232931
+ }), [t]);
232780
232932
  const { weeklyCostData, isLoadingWeeklyCostData, isErrorWeeklyCostData, monthlyCostData, isLoadingMonthlyCostData, isErrorMonthlyCostData, totalCostData, isLoadingTotalCostData, isErrorTotalCostData, detailedCostPerDayData, isLoadingDetailedCostPerDayData, detailedCostPerDayFilters, setDetailedCostPerDayFilters, detailedCostPerProviderData, isLoadingDetailedCostPerProviderData, detailedCostPerProviderFilters, setDetailedCostPerProviderFilters, detailedCostPerUserData, isLoadingDetailedCostPerUserData, detailedCostPerUserFilters, setDetailedCostPerUserFilters, costPerUserPage, setCostPerUserPage, costPerUserLimit, providerColors, detailedLLMCostData, isLoadingDetailedLLMCostData, detailedLLMCostFilters, setDetailedLLMCostFilters, } = useFinancial({ tenantKey, mentorId: currentMentorId, userId });
232781
232933
  // Pagination handlers
232782
232934
  const handleFirstPage = () => {
@@ -232871,7 +233023,7 @@ function SpendTab({ tenantKey, currentSPA, mentorId: currentMentorId, userId, on
232871
233023
  shown: (_p = (_o = detailedLLMCostData === null || detailedLLMCostData === void 0 ? void 0 : detailedLLMCostData.rows) === null || _o === void 0 ? void 0 : _o.length) !== null && _p !== void 0 ? _p : 0,
232872
233024
  }) })) : (jsx(EmptyStats, {})) })] }), jsx("div", { className: "grid grid-cols-1 gap-6", children: jsx(ChartCardWrapper, { title: t('analyticsAnalyticsFinancialStats.costPerUserTitle'), source: "clickhouse", label: t('analyticsAnalyticsFinancialStats.costPerUserLabel'), filters: detailedCostPerUserFilters, setOutsideFilters: setDetailedCostPerUserFilters, overflowAuto: true, height: "450px", description: t('analyticsAnalyticsFinancialStats.costPerUserDescription'), children: jsxs("div", { className: "space-y-4", children: [jsx("div", { className: "rounded-md", style: { borderColor: 'oklch(.922 0 0)', borderWidth: '1px' }, children: jsxs(Table, { children: [jsx(TableHeader, { children: jsxs(TableRow, { className: "h-[48px]", style: { borderColor: 'oklch(.922 0 0)' }, children: [jsx(TableHead, { className: "w-[250px]", children: t('analyticsAnalyticsFinancialStats.tableHeaderUserEmail') }), jsx(TableHead, { className: "w-[120px]", children: t('analyticsAnalyticsFinancialStats.tableHeaderTotalCost') }), hasSessions && (jsx(TableHead, { children: t('analyticsAnalyticsFinancialStats.tableHeaderSessions') })), hasLastActive && (jsx(TableHead, { className: "text-right", children: t('analyticsAnalyticsFinancialStats.tableHeaderLastActive') })), onOpenUserTraces && jsx(TableHead, { className: "w-[90px]" })] }) }), jsx(TableBody, { children: (_q = detailedCostPerUserData === null || detailedCostPerUserData === void 0 ? void 0 : detailedCostPerUserData.rows) === null || _q === void 0 ? void 0 : _q.map((user, index) => {
232873
233025
  var _a;
232874
- return (jsxs(TableRow, { className: cn(index % 2 === 0 ? 'bg-gray-50' : '', 'h-[48px]'), style: { borderColor: 'oklch(.922 0 0)' }, children: [jsx(TableCell, { className: "font-medium", children: jsx("div", { className: "flex items-center", children: (user === null || user === void 0 ? void 0 : user.email) || (user === null || user === void 0 ? void 0 : user.username) ? (jsx(UserProfileLink, { tenantKey: tenantKey, currentSPA: currentSPA, username: user.username, display: String(user.email || user.username) })) : (t('analyticsAnalyticsFinancialStats.anonymous')) }) }), jsxs(TableCell, { className: "font-medium", children: ["$", Number(user.total_cost || 0).toFixed(2)] }), hasSessions && jsx(TableCell, { children: (_a = user.sessions) !== null && _a !== void 0 ? _a : '\u2014' }), hasLastActive && (jsx(TableCell, { className: "text-right", children: user.last_active ? formatRelativeTime$2(user.last_active) : '-' })), onOpenUserTraces && (jsx(TableCell, { className: "text-right", children: (user === null || user === void 0 ? void 0 : user.username) || (user === null || user === void 0 ? void 0 : user.email) ? (jsx("button", { type: "button", className: "cursor-pointer text-sm font-semibold text-blue-600 hover:text-blue-700 hover:underline", onClick: () => onOpenUserTraces(String(user.username || user.email)), children: t('analyticsAnalyticsFinancialStats.viewTraces') })) : null }))] }, index));
233026
+ return (jsxs(TableRow, { className: cn(index % 2 === 0 ? 'bg-gray-50' : '', 'h-[48px]'), style: { borderColor: 'oklch(.922 0 0)' }, children: [jsx(TableCell, { className: "font-medium", children: jsx("div", { className: "flex items-center", children: jsx(UserProfileLink, { tenantKey: tenantKey, currentSPA: currentSPA, username: user.username, display: identify(user).label }) }) }), jsxs(TableCell, { className: "font-medium", children: ["$", Number(user.total_cost || 0).toFixed(2)] }), hasSessions && jsx(TableCell, { children: (_a = user.sessions) !== null && _a !== void 0 ? _a : '\u2014' }), hasLastActive && (jsx(TableCell, { className: "text-right", children: user.last_active ? formatRelativeTime$2(user.last_active) : '-' })), onOpenUserTraces && (jsx(TableCell, { className: "text-right", children: (user === null || user === void 0 ? void 0 : user.username) || (user === null || user === void 0 ? void 0 : user.user_email) || (user === null || user === void 0 ? void 0 : user.email) ? (jsx("button", { type: "button", className: "cursor-pointer text-sm font-semibold text-blue-600 hover:text-blue-700 hover:underline", onClick: () => onOpenUserTraces(String(user.username || user.user_email || user.email)), children: t('analyticsAnalyticsFinancialStats.viewTraces') })) : null }))] }, index));
232875
233027
  }) })] }) }), isLoadingDetailedCostPerUserData && (jsx("div", { className: "h-[60px]", children: jsx(ChartLoading, {}) })), !isLoadingDetailedCostPerUserData &&
232876
233028
  (!((_r = detailedCostPerUserData === null || detailedCostPerUserData === void 0 ? void 0 : detailedCostPerUserData.rows) === null || _r === void 0 ? void 0 : _r.length) ||
232877
233029
  ((_s = detailedCostPerUserData === null || detailedCostPerUserData === void 0 ? void 0 : detailedCostPerUserData.rows) === null || _s === void 0 ? void 0 : _s.length) === 0) && (jsx("div", { className: "h-[60px]", children: jsx(EmptyStats, {}) })), detailedCostPerUserData && detailedCostPerUserData.total_pages > 1 && (jsxs("div", { className: "flex items-center justify-between py-3", children: [jsx("div", { className: "text-sm text-gray-700", children: t('analyticsAnalyticsFinancialStats.paginationShowing', {
@@ -233362,7 +233514,7 @@ function SectionHeading({ icon, children }) {
233362
233514
  return (jsxs("h4", { className: "mb-2 flex items-center gap-1.5 text-xs font-semibold tracking-wide text-gray-700 uppercase", children: [icon, children] }));
233363
233515
  }
233364
233516
  const MODEL_KEYS = new Set(['llm_model', 'llm_provider']);
233365
- function MetadataSection({ metadata, renderModel, }) {
233517
+ function MetadataSection({ metadata, renderModel, tenantKey, mentorId, }) {
233366
233518
  var _a, _b;
233367
233519
  const t = useT();
233368
233520
  const labelFor = useFieldLabels();
@@ -233370,7 +233522,9 @@ function MetadataSection({ metadata, renderModel, }) {
233370
233522
  const model = ((_a = metadata.llm_model) !== null && _a !== void 0 ? _a : '').trim() || undefined;
233371
233523
  const provider = ((_b = metadata.llm_provider) !== null && _b !== void 0 ? _b : '').trim() || undefined;
233372
233524
  const hasModel = !!(model || provider);
233373
- return (jsxs("section", { "data-testid": "transcript-turn-metadata", children: [jsx(SectionHeading, { icon: jsx(Info$3, { className: "h-3.5 w-3.5", "aria-hidden": "true" }), children: t('analyticsTranscriptTurnDetails.metadata') }), hasModel && (jsx("div", { "data-testid": "transcript-turn-model", className: "mb-2", children: (renderModel !== null && renderModel !== void 0 ? renderModel : defaultRenderModel)({ model, provider }) })), jsx(DetailRows, { entries: rest, labelFor: labelFor })] }));
233525
+ return (jsxs("section", { "data-testid": "transcript-turn-metadata", children: [jsx(SectionHeading, { icon: jsx(Info$3, { className: "h-3.5 w-3.5", "aria-hidden": "true" }), children: t('analyticsTranscriptTurnDetails.metadata') }), hasModel && (jsx("div", { "data-testid": "transcript-turn-model", className: "mb-2", children: renderModel
233526
+ ? renderModel({ model, provider })
233527
+ : defaultRenderModel({ model, provider, tenantKey, mentorId }) })), jsx(DetailRows, { entries: rest, labelFor: labelFor })] }));
233374
233528
  }
233375
233529
  function RequestContextSection({ requestContext }) {
233376
233530
  const t = useT();
@@ -233385,7 +233539,7 @@ function RequestContextSection({ requestContext }) {
233385
233539
  */
233386
233540
  const defaultRenderDocuments = (documents) => (jsx(RetrievedDocumentsButton, { documents: toRetrievedDocuments(documents) }));
233387
233541
  const defaultRenderToolCalls = (toolCalls) => (jsx(ToolCallIndicator, { toolCalls: toToolCallInfos(toolCalls), showResults: true }));
233388
- const defaultRenderModel = ({ model, provider }) => (jsx(LlmModelBadge, { model: model, provider: provider }));
233542
+ const defaultRenderModel = ({ model, provider, tenantKey, mentorId, }) => jsx(LlmModelBadge, { model: model, provider: provider, tenantKey: tenantKey, mentorId: mentorId });
233389
233543
  const defaultRenderFiles = (files) => (jsx(TranscriptFileCards, { files: files }));
233390
233544
  /**
233391
233545
  * Collapsible "Sources / Tools / Metadata" panel for one AI turn of an admin
@@ -233395,7 +233549,7 @@ const defaultRenderFiles = (files) => (jsx(TranscriptFileCards, { files: files }
233395
233549
  * renders nothing at all when every field is null or empty, so it is safe to
233396
233550
  * drop under every AI bubble.
233397
233551
  */
233398
- function TranscriptTurnDetails({ turn, className, defaultOpen = false, renderDocuments, renderToolCalls, renderModel, renderFiles, }) {
233552
+ function TranscriptTurnDetails({ turn, className, defaultOpen = false, renderDocuments, renderToolCalls, renderModel, renderFiles, tenantKey, mentorId, }) {
233399
233553
  const t = useT();
233400
233554
  const labelFor = useFieldLabels();
233401
233555
  const [open, setOpen] = React__default.useState(defaultOpen);
@@ -233409,7 +233563,7 @@ function TranscriptTurnDetails({ turn, className, defaultOpen = false, renderDoc
233409
233563
  const requestContext = hasEntries(turn === null || turn === void 0 ? void 0 : turn.request_context) ? turn === null || turn === void 0 ? void 0 : turn.request_context : null;
233410
233564
  return (jsx(DetailCtx.Provider, { value: detailContext, children: jsxs(Collapsible, { open: open, onOpenChange: setOpen, className: cn('mt-2', className), "data-testid": "transcript-turn-details", children: [jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [jsx(CollapsibleTrigger, { asChild: true, children: jsxs(Button$1, { type: "button", variant: "outline", size: "sm", className: "flex items-center gap-2 border-gray-200 bg-white text-gray-700 hover:bg-gray-50", "aria-expanded": open, children: [jsx(ChevronDown, { className: cn('h-4 w-4 transition-transform', open && 'rotate-180'), "aria-hidden": "true" }), open
233411
233565
  ? t('analyticsTranscriptTurnDetails.hideDetails')
233412
- : t('analyticsTranscriptTurnDetails.showDetails')] }) }), documents.length > 0 && (jsx("div", { "data-testid": "transcript-turn-sources", className: "contents", children: (renderDocuments !== null && renderDocuments !== void 0 ? renderDocuments : defaultRenderDocuments)(documents) })), jsx(TranscriptRollupBadges, { documentsCount: 0, toolCallsCount: toolCalls.length })] }), jsxs(CollapsibleContent, { className: "mt-2 space-y-5 rounded-lg border border-gray-200 bg-white p-3", children: [toolCalls.length > 0 && (jsx("div", { "data-testid": "transcript-turn-tools", children: (renderToolCalls !== null && renderToolCalls !== void 0 ? renderToolCalls : defaultRenderToolCalls)(toolCalls) })), metadata && jsx(MetadataSection, { metadata: metadata, renderModel: renderModel }), requestContext && jsx(RequestContextSection, { requestContext: requestContext })] })] }) }));
233566
+ : t('analyticsTranscriptTurnDetails.showDetails')] }) }), documents.length > 0 && (jsx("div", { "data-testid": "transcript-turn-sources", className: "contents", children: (renderDocuments !== null && renderDocuments !== void 0 ? renderDocuments : defaultRenderDocuments)(documents) })), jsx(TranscriptRollupBadges, { documentsCount: 0, toolCallsCount: toolCalls.length })] }), jsxs(CollapsibleContent, { className: "mt-2 space-y-5 rounded-lg border border-gray-200 bg-white p-3", children: [toolCalls.length > 0 && (jsx("div", { "data-testid": "transcript-turn-tools", children: (renderToolCalls !== null && renderToolCalls !== void 0 ? renderToolCalls : defaultRenderToolCalls)(toolCalls) })), metadata && (jsx(MetadataSection, { metadata: metadata, renderModel: renderModel, tenantKey: tenantKey, mentorId: mentorId })), requestContext && jsx(RequestContextSection, { requestContext: requestContext })] })] }) }));
233413
233567
  }
233414
233568
  /**
233415
233569
  * "Documents · N" / "Tools · N" chips for a conversation row, from the
@@ -233425,118 +233579,6 @@ function TranscriptRollupBadges({ documentsCount, toolCallsCount, className, })
233425
233579
  return (jsxs("span", { className: cn('inline-flex flex-wrap items-center gap-1.5', className), "data-testid": "transcript-rollup-badges", children: [documents > 0 && (jsxs(Badge, { variant: "outline", className: "gap-1 border-transparent bg-blue-50 font-normal text-blue-700", "data-testid": "transcript-sources-badge", children: [jsx(FileText, { className: "h-3 w-3", "aria-hidden": "true" }), t('analyticsTranscriptTurnDetails.sourcesBadge', { count: documents })] })), tools > 0 && (jsxs(Badge, { variant: "outline", className: "gap-1 border-transparent bg-amber-50 font-normal text-amber-800", "data-testid": "transcript-tools-badge", children: [jsx(Wrench, { className: "h-3 w-3", "aria-hidden": "true" }), t('analyticsTranscriptTurnDetails.toolsBadge', { count: tools })] }))] }));
233426
233580
  }
233427
233581
 
233428
- /**
233429
- * One place that decides how a user is shown.
233430
- *
233431
- * Backend payloads name their user in a dozen ways — `email` / `lti_email` /
233432
- * `user_email`, `username` / `student` / `lti_username`, `name` /
233433
- * `full_name` / `user_full_name`, or a nested `user` object — and most of
233434
- * them can be empty, `null`, or a placeholder like "None". Every surface used
233435
- * to pick through those on its own. `resolveUserIdentity()` takes whatever
233436
- * the payload is and returns what to show, what to open a profile on, and
233437
- * the avatar initial, so the rule lives here and can be tweaked once.
233438
- *
233439
- * Default rule: email → full name → username → the anonymous label.
233440
- */
233441
- const DEFAULT_USER_IDENTITY_ORDER = ['email', 'name', 'username'];
233442
- const DEFAULT_ANONYMOUS_LABEL = 'Anonymous';
233443
- /** Values backends emit instead of "nothing". Compared case-insensitively. */
233444
- const PLACEHOLDERS = new Set([
233445
- '',
233446
- 'none',
233447
- 'null',
233448
- 'undefined',
233449
- 'n/a',
233450
- 'na',
233451
- '-',
233452
- 'anonymous',
233453
- 'anonymoususer',
233454
- ]);
233455
- /**
233456
- * `local@domain.tld` with no whitespace, checked with a single pass rather than
233457
- * a backtracking pattern (CodeQL flags `[^@]+@[^@]+\.[^@]+` as polynomial on
233458
- * adversarial input).
233459
- */
233460
- const isEmailLike = (value) => {
233461
- const text = value.trim();
233462
- if (!text || /\s/.test(text))
233463
- return false;
233464
- const at = text.indexOf('@');
233465
- if (at <= 0 || at !== text.lastIndexOf('@') || at === text.length - 1)
233466
- return false;
233467
- const domain = text.slice(at + 1);
233468
- const dot = domain.indexOf('.');
233469
- // A dot somewhere inside the domain, with something after the last one.
233470
- return dot > 0 && !domain.endsWith('.');
233471
- };
233472
- /** Trimmed string, or undefined for blanks, non-strings and placeholders. */
233473
- const clean = (value) => {
233474
- if (typeof value !== 'string')
233475
- return undefined;
233476
- const trimmed = value.trim();
233477
- return PLACEHOLDERS.has(trimmed.toLowerCase()) ? undefined : trimmed;
233478
- };
233479
- const firstOf = (...candidates) => {
233480
- for (const candidate of candidates) {
233481
- const value = clean(candidate);
233482
- if (value)
233483
- return value;
233484
- }
233485
- return undefined;
233486
- };
233487
- const nested = (source) => source.user && typeof source.user === 'object' ? source.user : undefined;
233488
- /**
233489
- * Resolve how a user should be displayed from any payload that names one.
233490
- *
233491
- * - `email`: `lti_email`, `email`, `user_email`, the nested `user`'s email —
233492
- * or a username field that turns out to hold an email address.
233493
- * - `username`: `lti_username`, `username`, `student`, `user_username`, the
233494
- * nested `user`'s username, or a bare string/number `user` value.
233495
- * - `name`: `user_full_name`, `full_name`, `name`, `first_name last_name`, or
233496
- * the nested `user`'s name.
233497
- * - `profileUsername`: the first *platform* username (never an LTI claim).
233498
- *
233499
- * A string source is treated as an email when it looks like one, else as a
233500
- * username. Placeholder strings ("None", "-", "anonymous"…) count as absent.
233501
- */
233502
- function resolveUserIdentity(source, options = {}) {
233503
- var _a;
233504
- const anonymousLabel = options.anonymousLabel || DEFAULT_ANONYMOUS_LABEL;
233505
- const order = ((_a = options.order) === null || _a === void 0 ? void 0 : _a.length) ? options.order : DEFAULT_USER_IDENTITY_ORDER;
233506
- const record = source && typeof source === 'object' ? source : { user: source !== null && source !== void 0 ? source : null };
233507
- const inner = nested(record);
233508
- // A bare `user` value is an id or username; numeric ids are still shown.
233509
- const bareUser = typeof record.user === 'number' && Number.isFinite(record.user)
233510
- ? String(record.user)
233511
- : typeof record.user === 'string'
233512
- ? clean(record.user)
233513
- : undefined;
233514
- let email = firstOf(record.lti_email, record.email, record.user_email, inner === null || inner === void 0 ? void 0 : inner.lti_email, inner === null || inner === void 0 ? void 0 : inner.email, inner === null || inner === void 0 ? void 0 : inner.user_email);
233515
- const platformUsername = firstOf(record.username, record.student, record.user_username, inner === null || inner === void 0 ? void 0 : inner.username, inner === null || inner === void 0 ? void 0 : inner.student, inner === null || inner === void 0 ? void 0 : inner.user_username, bareUser && !isEmailLike(bareUser) ? bareUser : undefined);
233516
- let username = firstOf(record.lti_username, inner === null || inner === void 0 ? void 0 : inner.lti_username, platformUsername);
233517
- const composed = [clean(record.first_name), clean(record.last_name)].filter(Boolean).join(' ');
233518
- const name = firstOf(record.user_full_name, record.full_name, record.name, composed, inner === null || inner === void 0 ? void 0 : inner.user_full_name, inner === null || inner === void 0 ? void 0 : inner.full_name, inner === null || inner === void 0 ? void 0 : inner.name);
233519
- // A bare string that looks like an email is an email, and so is a
233520
- // "username" field that actually carries one.
233521
- if (!email && bareUser && isEmailLike(bareUser))
233522
- email = bareUser;
233523
- if (!email && username && isEmailLike(username)) {
233524
- email = username;
233525
- username = undefined;
233526
- }
233527
- const facts = { email, username, name };
233528
- const label = order.map((field) => facts[field]).find(Boolean);
233529
- return {
233530
- label: label !== null && label !== void 0 ? label : anonymousLabel,
233531
- email,
233532
- username,
233533
- name,
233534
- profileUsername: platformUsername,
233535
- initial: (label !== null && label !== void 0 ? label : anonymousLabel).charAt(0).toUpperCase() || 'A',
233536
- isAnonymous: !label,
233537
- };
233538
- }
233539
-
233540
233582
  // Utility function to format relative time
233541
233583
  const formatRelativeTime = (dateString, t) => {
233542
233584
  const date = new Date(dateString);
@@ -233582,8 +233624,13 @@ const getMessageContent = (content) => {
233582
233624
  * in the order the caller asks for; "-" when the row names nobody.
233583
233625
  */
233584
233626
  const identifyUser = (source, order) => resolveUserIdentity(source, { order, anonymousLabel: '-' }).label;
233585
- /** The model as the nav bar shows it (logo + display name); hosts may override via `turnDetails.renderModel`. */
233586
- const renderModelBadge = ({ model, provider }) => (jsx(LlmModelBadge, { model: model, provider: provider }));
233627
+ /**
233628
+ * The model as the nav bar shows it (logo + display name); hosts may override
233629
+ * via `turnDetails.renderModel`. The catalogue is read for the tenant and the
233630
+ * agent the conversation ran on — route params only carry those on the mentor
233631
+ * SPA, so analytics passes them explicitly.
233632
+ */
233633
+ const renderModelBadge = ({ model, provider, tenantKey, mentorId, }) => jsx(LlmModelBadge, { model: model, provider: provider, tenantKey: tenantKey, mentorId: mentorId });
233587
233634
  /** Newer deployments name the provider on rows and summaries; older ones only the model. */
233588
233635
  const providerOf = (source) => {
233589
233636
  const value = source === null || source === void 0 ? void 0 : source.llm_provider;
@@ -233619,7 +233666,7 @@ function TranscriptDocumentsButton({ detail, className, }) {
233619
233666
  return (jsx(RetrievedDocumentsButton, { sessionId: sessionId, documents: documents.length > 0 ? documents : undefined, className: className }));
233620
233667
  }
233621
233668
  function AnalyticsTranscriptsStats({ tenantKey, currentSPA, mentorId, selectedMentorId, usergroupIds, turnDetails, }) {
233622
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
233669
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
233623
233670
  const t = useT();
233624
233671
  const currentMentorId = useScopedMentorId(selectedMentorId, mentorId);
233625
233672
  const [isModalOpen, setIsModalOpen] = useState(false);
@@ -233627,6 +233674,10 @@ function AnalyticsTranscriptsStats({ tenantKey, currentSPA, mentorId, selectedMe
233627
233674
  const router = useRouter();
233628
233675
  const pathname = usePathname();
233629
233676
  const { transcriptsMessagesData, isLoadingTranscriptsMessagesData, isFetchingTranscriptsMessagesData, filters, setFilters, handleClearFilters, currentPage, handlePageChange, selectedTranscript, setSelectedTranscript, transcriptsMessagesDetailsData, isLoadingTranscriptsMessagesDetailsData, isErrorTranscriptsMessagesDetailsData, transcriptsConversationHeadlineData, isLoadingTranscriptsConversationHeadlineData, } = useTranscripts({ tenantKey, mentorId: currentMentorId, usergroupIds });
233677
+ // The agent the open transcript ran on — the catalogue scope for its model
233678
+ // badge. Hosts that browse across agents (the LMS analytics route) pass no
233679
+ // mentorId, so the transcript's own agent is the only correct scope.
233680
+ const detailMentorId = ((_a = transcriptsMessagesDetailsData === null || transcriptsMessagesDetailsData === void 0 ? void 0 : transcriptsMessagesDetailsData.summary) === null || _a === void 0 ? void 0 : _a.mentor_unique_id) || currentMentorId;
233630
233681
  // Read session_id from URL on mount and select the transcript
233631
233682
  useEffect(() => {
233632
233683
  const sessionIdParam = searchParams === null || searchParams === void 0 ? void 0 : searchParams.get('session_id');
@@ -233739,15 +233790,17 @@ function AnalyticsTranscriptsStats({ tenantKey, currentSPA, mentorId, selectedMe
233739
233790
  ? ((_e = turnDetails === null || turnDetails === void 0 ? void 0 : turnDetails.renderModel) !== null && _e !== void 0 ? _e : renderModelBadge)({
233740
233791
  model: transcriptsMessagesDetailsData.summary.model,
233741
233792
  provider: (_f = providerOf(transcriptsMessagesDetailsData.summary)) !== null && _f !== void 0 ? _f : providerOfTurns(transcriptsMessagesDetailsData.messages),
233793
+ tenantKey,
233794
+ mentorId: detailMentorId,
233742
233795
  })
233743
233796
  : '-'] }), jsx(TranscriptDocumentsButton, { detail: transcriptsMessagesDetailsData, className: "mt-2" })] })] }), jsx("div", { className: "flex-1 overflow-y-auto space-y-4 min-h-0", children: ((_g = transcriptsMessagesDetailsData === null || transcriptsMessagesDetailsData === void 0 ? void 0 : transcriptsMessagesDetailsData.messages) === null || _g === void 0 ? void 0 : _g.length) > 0 ? ((_h = transcriptsMessagesDetailsData === null || transcriptsMessagesDetailsData === void 0 ? void 0 : transcriptsMessagesDetailsData.messages) === null || _h === void 0 ? void 0 : _h.map((message, index) => {
233744
233797
  var _a, _b;
233745
- return (jsxs(React__default.Fragment, { children: [((message === null || message === void 0 ? void 0 : message.human) || !!((_a = message === null || message === void 0 ? void 0 : message.human_files) === null || _a === void 0 ? void 0 : _a.length)) && (jsxs("div", { className: `p-3 rounded-lg bg-blue-100 ml-4`, children: [jsx("div", { className: "flex justify-between items-start mb-1", children: jsx("span", { className: "font-medium text-sm", children: t('analyticsAnalyticsTranscriptsStats.roleUser') }) }), message.human && (jsx("p", { className: "text-sm", children: getMessageContent(message.human) })), jsx(TranscriptFiles, { files: message.human_files })] }, `${index}-human`)), ((message === null || message === void 0 ? void 0 : message.ai) || !!((_b = message === null || message === void 0 ? void 0 : message.ai_files) === null || _b === void 0 ? void 0 : _b.length)) && (jsxs("div", { className: `p-3 rounded-lg bg-white mr-4`, children: [jsx("div", { className: "flex justify-between items-start mb-1", children: jsx("span", { className: "font-medium text-sm", children: t('analyticsAnalyticsTranscriptsStats.roleAgent') }) }), message.ai && jsx(Markdown, { children: getMessageContent(message.ai) }), jsx(TranscriptFiles, { files: message.ai_files }), jsx(TranscriptTurnDetails, { turn: message, ...turnDetails })] }, `${index}-ai`))] }, index));
233798
+ return (jsxs(React__default.Fragment, { children: [((message === null || message === void 0 ? void 0 : message.human) || !!((_a = message === null || message === void 0 ? void 0 : message.human_files) === null || _a === void 0 ? void 0 : _a.length)) && (jsxs("div", { className: `p-3 rounded-lg bg-blue-100 ml-4`, children: [jsx("div", { className: "flex justify-between items-start mb-1", children: jsx("span", { className: "font-medium text-sm", children: t('analyticsAnalyticsTranscriptsStats.roleUser') }) }), message.human && (jsx("p", { className: "text-sm", children: getMessageContent(message.human) })), jsx(TranscriptFiles, { files: message.human_files })] }, `${index}-human`)), ((message === null || message === void 0 ? void 0 : message.ai) || !!((_b = message === null || message === void 0 ? void 0 : message.ai_files) === null || _b === void 0 ? void 0 : _b.length)) && (jsxs("div", { className: `p-3 rounded-lg bg-white mr-4`, children: [jsx("div", { className: "flex justify-between items-start mb-1", children: jsx("span", { className: "font-medium text-sm", children: t('analyticsAnalyticsTranscriptsStats.roleAgent') }) }), message.ai && jsx(Markdown, { children: getMessageContent(message.ai) }), jsx(TranscriptFiles, { files: message.ai_files }), jsx(TranscriptTurnDetails, { turn: message, tenantKey: tenantKey, mentorId: detailMentorId, ...turnDetails })] }, `${index}-ai`))] }, index));
233746
233799
  })) : (jsx("div", { className: "text-center text-gray-500 py-8", children: jsx("p", { children: t('analyticsAnalyticsTranscriptsStats.noMessages') }) })) })] })) }));
233747
233800
  };
233748
- return (jsxs("div", { className: "space-y-8", children: [jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 mb-6", children: [jsx(StatCard, { title: t('analyticsAnalyticsTranscriptsStats.avgMessagesPerConversation'), value: ((_a = transcriptsConversationHeadlineData === null || transcriptsConversationHeadlineData === void 0 ? void 0 : transcriptsConversationHeadlineData.avg_messages_per_conversation) === null || _a === void 0 ? void 0 : _a.value) || 0, percentage: ((_b = transcriptsConversationHeadlineData === null || transcriptsConversationHeadlineData === void 0 ? void 0 : transcriptsConversationHeadlineData.avg_messages_per_conversation) === null || _b === void 0 ? void 0 : _b.percentage_change) ||
233749
- undefined, loading: isLoadingTranscriptsConversationHeadlineData, description: t('analyticsAnalyticsTranscriptsStats.avgMessagesPerConversationDescription') }), jsx(StatCard, { title: t('analyticsAnalyticsTranscriptsStats.avgCostPerConversation'), value: Number(((_c = transcriptsConversationHeadlineData === null || transcriptsConversationHeadlineData === void 0 ? void 0 : transcriptsConversationHeadlineData.avg_cost_per_conversation) === null || _c === void 0 ? void 0 : _c.value) || 0).toFixed(2), percentage: ((_d = transcriptsConversationHeadlineData === null || transcriptsConversationHeadlineData === void 0 ? void 0 : transcriptsConversationHeadlineData.avg_cost_per_conversation) === null || _d === void 0 ? void 0 : _d.percentage_change) ||
233750
- undefined, showDollarSign: true, loading: isLoadingTranscriptsConversationHeadlineData, description: t('analyticsAnalyticsTranscriptsStats.avgCostPerConversationDescription') }), jsx(StatCard, { title: t('analyticsAnalyticsTranscriptsStats.avgRating'), value: ((_e = transcriptsConversationHeadlineData === null || transcriptsConversationHeadlineData === void 0 ? void 0 : transcriptsConversationHeadlineData.avg_rating) === null || _e === void 0 ? void 0 : _e.value) || 0, percentage: ((_f = transcriptsConversationHeadlineData === null || transcriptsConversationHeadlineData === void 0 ? void 0 : transcriptsConversationHeadlineData.avg_rating) === null || _f === void 0 ? void 0 : _f.percentage_change) || undefined, loading: isLoadingTranscriptsConversationHeadlineData, description: t('analyticsAnalyticsTranscriptsStats.avgRatingDescription') })] }), jsxs("div", { className: "flex h-[calc(100vh-120px)]", children: [jsxs("div", { className: "w-full md:w-2/3 md:pr-6 overflow-y-auto md:border-r border-gray-200 [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]", children: [jsx("div", { className: "mb-6", children: jsxs("div", { className: "flex gap-2 mb-4 items-center", children: [jsxs("div", { className: "relative", children: [jsx(Search, { className: "absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" }), jsx(Input, { placeholder: t('analyticsAnalyticsTranscriptsStats.placeholderTopics'), value: filters.topicSearch, onChange: (e) => setFilters({ ...filters, topicSearch: e.target.value }), className: "pl-10 w-[200px]" })] }), jsx("div", { className: "w-[200px]", children: jsx(UserFilterSelect, { tenantKey: tenantKey, value: filters.userSearch, onChange: (username) => setFilters({ ...filters, userSearch: username }), showSelfOption: false, className: "bg-transparent" }) }), isFetchingTranscriptsMessagesData && (filters.userSearch || filters.topicSearch) ? (jsx("div", { className: "animate-spin rounded-full h-5 w-5 border-b-2 border-blue-500 ml-2", "aria-hidden": "true" })) : (jsx(Button$1, { variant: "outline", onClick: handleClearFilters, className: "whitespace-nowrap", children: t('analyticsAnalyticsTranscriptsStats.clearAll') }))] }) }), jsxs("div", { className: "bg-gray-50 p-4 rounded-lg mb-6", children: [jsxs("div", { className: "flex flex-wrap gap-x-8 gap-y-2 text-sm text-gray-600", children: [jsxs("div", { "aria-label": t('analyticsAnalyticsTranscriptsStats.ariaTotalConversations'), children: [transcriptsMessagesData === null || transcriptsMessagesData === void 0 ? void 0 : transcriptsMessagesData.summary.total_conversations, ' ', t('analyticsAnalyticsTranscriptsStats.conversations')] }), jsx("div", { children: "\u2022" }), jsxs("div", { children: [transcriptsMessagesData === null || transcriptsMessagesData === void 0 ? void 0 : transcriptsMessagesData.summary.user_queries, ' ', t('analyticsAnalyticsTranscriptsStats.userQueries')] }), jsx("div", { children: "\u2022" }), jsxs("div", { children: [transcriptsMessagesData === null || transcriptsMessagesData === void 0 ? void 0 : transcriptsMessagesData.summary.assistant_responses, ' ', t('analyticsAnalyticsTranscriptsStats.assistantResponses')] })] }), jsx("div", { className: "flex flex-wrap gap-x-8 gap-y-2 text-sm text-gray-600 mt-2", children: jsxs("div", { children: [transcriptsMessagesData === null || transcriptsMessagesData === void 0 ? void 0 : transcriptsMessagesData.summary.average_sentiment_score, ' ', t('analyticsAnalyticsTranscriptsStats.averageSentimentScore')] }) })] }), jsx("div", { className: "flex flex-col gap-4", children: isLoadingTranscriptsMessagesData ? (
233801
+ return (jsxs("div", { className: "space-y-8", children: [jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-4 mb-6", children: [jsx(StatCard, { title: t('analyticsAnalyticsTranscriptsStats.avgMessagesPerConversation'), value: ((_b = transcriptsConversationHeadlineData === null || transcriptsConversationHeadlineData === void 0 ? void 0 : transcriptsConversationHeadlineData.avg_messages_per_conversation) === null || _b === void 0 ? void 0 : _b.value) || 0, percentage: ((_c = transcriptsConversationHeadlineData === null || transcriptsConversationHeadlineData === void 0 ? void 0 : transcriptsConversationHeadlineData.avg_messages_per_conversation) === null || _c === void 0 ? void 0 : _c.percentage_change) ||
233802
+ undefined, loading: isLoadingTranscriptsConversationHeadlineData, description: t('analyticsAnalyticsTranscriptsStats.avgMessagesPerConversationDescription') }), jsx(StatCard, { title: t('analyticsAnalyticsTranscriptsStats.avgCostPerConversation'), value: Number(((_d = transcriptsConversationHeadlineData === null || transcriptsConversationHeadlineData === void 0 ? void 0 : transcriptsConversationHeadlineData.avg_cost_per_conversation) === null || _d === void 0 ? void 0 : _d.value) || 0).toFixed(2), percentage: ((_e = transcriptsConversationHeadlineData === null || transcriptsConversationHeadlineData === void 0 ? void 0 : transcriptsConversationHeadlineData.avg_cost_per_conversation) === null || _e === void 0 ? void 0 : _e.percentage_change) ||
233803
+ undefined, showDollarSign: true, loading: isLoadingTranscriptsConversationHeadlineData, description: t('analyticsAnalyticsTranscriptsStats.avgCostPerConversationDescription') }), jsx(StatCard, { title: t('analyticsAnalyticsTranscriptsStats.avgRating'), value: ((_f = transcriptsConversationHeadlineData === null || transcriptsConversationHeadlineData === void 0 ? void 0 : transcriptsConversationHeadlineData.avg_rating) === null || _f === void 0 ? void 0 : _f.value) || 0, percentage: ((_g = transcriptsConversationHeadlineData === null || transcriptsConversationHeadlineData === void 0 ? void 0 : transcriptsConversationHeadlineData.avg_rating) === null || _g === void 0 ? void 0 : _g.percentage_change) || undefined, loading: isLoadingTranscriptsConversationHeadlineData, description: t('analyticsAnalyticsTranscriptsStats.avgRatingDescription') })] }), jsxs("div", { className: "flex h-[calc(100vh-120px)]", children: [jsxs("div", { className: "w-full md:w-2/3 md:pr-6 overflow-y-auto md:border-r border-gray-200 [&::-webkit-scrollbar]:hidden [-ms-overflow-style:none] [scrollbar-width:none]", children: [jsx("div", { className: "mb-6", children: jsxs("div", { className: "flex gap-2 mb-4 items-center", children: [jsxs("div", { className: "relative", children: [jsx(Search, { className: "absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 h-4 w-4" }), jsx(Input, { placeholder: t('analyticsAnalyticsTranscriptsStats.placeholderTopics'), value: filters.topicSearch, onChange: (e) => setFilters({ ...filters, topicSearch: e.target.value }), className: "pl-10 w-[200px]" })] }), jsx("div", { className: "w-[200px]", children: jsx(UserFilterSelect, { tenantKey: tenantKey, value: filters.userSearch, onChange: (username) => setFilters({ ...filters, userSearch: username }), showSelfOption: false, className: "bg-transparent" }) }), isFetchingTranscriptsMessagesData && (filters.userSearch || filters.topicSearch) ? (jsx("div", { className: "animate-spin rounded-full h-5 w-5 border-b-2 border-blue-500 ml-2", "aria-hidden": "true" })) : (jsx(Button$1, { variant: "outline", onClick: handleClearFilters, className: "whitespace-nowrap", children: t('analyticsAnalyticsTranscriptsStats.clearAll') }))] }) }), jsxs("div", { className: "bg-gray-50 p-4 rounded-lg mb-6", children: [jsxs("div", { className: "flex flex-wrap gap-x-8 gap-y-2 text-sm text-gray-600", children: [jsxs("div", { "aria-label": t('analyticsAnalyticsTranscriptsStats.ariaTotalConversations'), children: [transcriptsMessagesData === null || transcriptsMessagesData === void 0 ? void 0 : transcriptsMessagesData.summary.total_conversations, ' ', t('analyticsAnalyticsTranscriptsStats.conversations')] }), jsx("div", { children: "\u2022" }), jsxs("div", { children: [transcriptsMessagesData === null || transcriptsMessagesData === void 0 ? void 0 : transcriptsMessagesData.summary.user_queries, ' ', t('analyticsAnalyticsTranscriptsStats.userQueries')] }), jsx("div", { children: "\u2022" }), jsxs("div", { children: [transcriptsMessagesData === null || transcriptsMessagesData === void 0 ? void 0 : transcriptsMessagesData.summary.assistant_responses, ' ', t('analyticsAnalyticsTranscriptsStats.assistantResponses')] })] }), jsx("div", { className: "flex flex-wrap gap-x-8 gap-y-2 text-sm text-gray-600 mt-2", children: jsxs("div", { children: [transcriptsMessagesData === null || transcriptsMessagesData === void 0 ? void 0 : transcriptsMessagesData.summary.average_sentiment_score, ' ', t('analyticsAnalyticsTranscriptsStats.averageSentimentScore')] }) })] }), jsx("div", { className: "flex flex-col gap-4", children: isLoadingTranscriptsMessagesData ? (
233751
233804
  // Loading state
233752
233805
  jsx("div", { className: "space-y-4", children: Array.from({ length: 3 }).map((_, index) => (jsxs(Card$1, { className: "p-4 animate-pulse", children: [jsx("div", { className: "mb-3", children: jsx("div", { className: "h-4 bg-gray-200 rounded w-3/4" }) }), jsxs("div", { className: "flex flex-wrap gap-2 mb-3", children: [jsx("div", { className: "h-6 bg-gray-200 rounded w-16" }), jsx("div", { className: "h-6 bg-gray-200 rounded w-20" })] }), jsxs("div", { className: "flex flex-wrap gap-x-6 gap-y-2 mb-2", children: [jsx("div", { className: "h-4 bg-gray-200 rounded w-24" }), jsx("div", { className: "h-4 bg-gray-200 rounded w-20" })] }), jsxs("div", { className: "flex flex-wrap gap-x-6 gap-y-2", children: [jsx("div", { className: "h-4 bg-gray-200 rounded w-20" }), jsx("div", { className: "h-4 bg-gray-200 rounded w-24" })] })] }, index))) })) : (transcriptsMessagesData === null || transcriptsMessagesData === void 0 ? void 0 : transcriptsMessagesData.results) && transcriptsMessagesData.results.length > 0 ? (
233753
233806
  // Results
@@ -233768,6 +233821,8 @@ function AnalyticsTranscriptsStats({ tenantKey, currentSPA, mentorId, selectedMe
233768
233821
  ? ((_c = turnDetails === null || turnDetails === void 0 ? void 0 : turnDetails.renderModel) !== null && _c !== void 0 ? _c : renderModelBadge)({
233769
233822
  model: transcript.model,
233770
233823
  provider: providerOf(transcript),
233824
+ tenantKey,
233825
+ mentorId: (transcript === null || transcript === void 0 ? void 0 : transcript.mentor_unique_id) || currentMentorId,
233771
233826
  })
233772
233827
  : '-'] }) }), jsxs("div", { className: "flex flex-wrap gap-x-6 gap-y-2 text-xs text-gray-500", children: [jsxs("div", { children: [transcript.message_count, " ", t('analyticsAnalyticsTranscriptsStats.messages')] }), jsx("div", { children: "\u2022" }), jsxs("div", { children: ["$", Number((transcript === null || transcript === void 0 ? void 0 : transcript.cost) || 0).toFixed(2), ' ', t('analyticsAnalyticsTranscriptsStats.estimatedCost')] }), (transcript === null || transcript === void 0 ? void 0 : transcript.created_at) && (jsxs(Fragment$1, { children: [jsx("div", { children: "\u2022" }), jsxs("div", { children: [t('analyticsAnalyticsTranscriptsStats.created'), ' ', formatRelativeTime(transcript.created_at, t)] })] }))] })] }, transcript.session));
233773
233828
  })) : (
@@ -233805,14 +233860,16 @@ function AnalyticsTranscriptsStats({ tenantKey, currentSPA, mentorId, selectedMe
233805
233860
  /* Error or no data state */
233806
233861
  jsx("div", { className: "flex items-center justify-center h-full text-gray-400", children: jsxs("div", { className: "text-center", children: [jsx(X$1, { className: "h-6 w-6 mx-auto mb-2 text-red-400" }), jsx("p", { children: t('analyticsAnalyticsTranscriptsStats.noTranscriptDetails') }), jsx("p", { className: "text-sm mt-1", children: t('analyticsAnalyticsTranscriptsStats.unableToLoadTranscript') })] }) })) : (
233807
233862
  /* Data loaded successfully */
233808
- jsxs("div", { className: "space-y-4", children: [jsx("div", { className: "mb-4", children: jsxs("div", { className: "text-sm text-gray-500 space-y-1", children: [jsxs("div", { children: [t('analyticsAnalyticsTranscriptsStats.labelUser'), ' ', jsx(UserProfileLink, { currentSPA: currentSPA, tenantKey: tenantKey, username: (_g = transcriptsMessagesDetailsData === null || transcriptsMessagesDetailsData === void 0 ? void 0 : transcriptsMessagesDetailsData.summary) === null || _g === void 0 ? void 0 : _g.username, display: identifyUser(transcriptsMessagesDetailsData === null || transcriptsMessagesDetailsData === void 0 ? void 0 : transcriptsMessagesDetailsData.summary) })] }), jsxs("div", { children: [t('analyticsAnalyticsTranscriptsStats.labelAgent'), ' ', jsx(AgentScopeLink, { uniqueId: (_h = transcriptsMessagesDetailsData === null || transcriptsMessagesDetailsData === void 0 ? void 0 : transcriptsMessagesDetailsData.summary) === null || _h === void 0 ? void 0 : _h.mentor_unique_id, name: (_j = transcriptsMessagesDetailsData === null || transcriptsMessagesDetailsData === void 0 ? void 0 : transcriptsMessagesDetailsData.summary) === null || _j === void 0 ? void 0 : _j.mentor })] }), jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [t('analyticsAnalyticsTranscriptsStats.labelModel'), ' ', ((_k = transcriptsMessagesDetailsData === null || transcriptsMessagesDetailsData === void 0 ? void 0 : transcriptsMessagesDetailsData.summary) === null || _k === void 0 ? void 0 : _k.model)
233809
- ? ((_l = turnDetails === null || turnDetails === void 0 ? void 0 : turnDetails.renderModel) !== null && _l !== void 0 ? _l : renderModelBadge)({
233863
+ jsxs("div", { className: "space-y-4", children: [jsx("div", { className: "mb-4", children: jsxs("div", { className: "text-sm text-gray-500 space-y-1", children: [jsxs("div", { children: [t('analyticsAnalyticsTranscriptsStats.labelUser'), ' ', jsx(UserProfileLink, { currentSPA: currentSPA, tenantKey: tenantKey, username: (_h = transcriptsMessagesDetailsData === null || transcriptsMessagesDetailsData === void 0 ? void 0 : transcriptsMessagesDetailsData.summary) === null || _h === void 0 ? void 0 : _h.username, display: identifyUser(transcriptsMessagesDetailsData === null || transcriptsMessagesDetailsData === void 0 ? void 0 : transcriptsMessagesDetailsData.summary) })] }), jsxs("div", { children: [t('analyticsAnalyticsTranscriptsStats.labelAgent'), ' ', jsx(AgentScopeLink, { uniqueId: (_j = transcriptsMessagesDetailsData === null || transcriptsMessagesDetailsData === void 0 ? void 0 : transcriptsMessagesDetailsData.summary) === null || _j === void 0 ? void 0 : _j.mentor_unique_id, name: (_k = transcriptsMessagesDetailsData === null || transcriptsMessagesDetailsData === void 0 ? void 0 : transcriptsMessagesDetailsData.summary) === null || _k === void 0 ? void 0 : _k.mentor })] }), jsxs("div", { className: "flex flex-wrap items-center gap-1.5", children: [t('analyticsAnalyticsTranscriptsStats.labelModel'), ' ', ((_l = transcriptsMessagesDetailsData === null || transcriptsMessagesDetailsData === void 0 ? void 0 : transcriptsMessagesDetailsData.summary) === null || _l === void 0 ? void 0 : _l.model)
233864
+ ? ((_m = turnDetails === null || turnDetails === void 0 ? void 0 : turnDetails.renderModel) !== null && _m !== void 0 ? _m : renderModelBadge)({
233810
233865
  model: transcriptsMessagesDetailsData.summary.model,
233811
- provider: (_m = providerOf(transcriptsMessagesDetailsData.summary)) !== null && _m !== void 0 ? _m : providerOfTurns(transcriptsMessagesDetailsData.messages),
233866
+ provider: (_o = providerOf(transcriptsMessagesDetailsData.summary)) !== null && _o !== void 0 ? _o : providerOfTurns(transcriptsMessagesDetailsData.messages),
233867
+ tenantKey,
233868
+ mentorId: detailMentorId,
233812
233869
  })
233813
- : '-'] }), jsx(TranscriptDocumentsButton, { detail: transcriptsMessagesDetailsData, className: "mt-2" })] }) }), jsx("div", { className: "space-y-4 pb-4", children: ((_o = transcriptsMessagesDetailsData === null || transcriptsMessagesDetailsData === void 0 ? void 0 : transcriptsMessagesDetailsData.messages) === null || _o === void 0 ? void 0 : _o.length) > 0 ? ((_p = transcriptsMessagesDetailsData === null || transcriptsMessagesDetailsData === void 0 ? void 0 : transcriptsMessagesDetailsData.messages) === null || _p === void 0 ? void 0 : _p.map((message, index) => {
233870
+ : '-'] }), jsx(TranscriptDocumentsButton, { detail: transcriptsMessagesDetailsData, className: "mt-2" })] }) }), jsx("div", { className: "space-y-4 pb-4", children: ((_p = transcriptsMessagesDetailsData === null || transcriptsMessagesDetailsData === void 0 ? void 0 : transcriptsMessagesDetailsData.messages) === null || _p === void 0 ? void 0 : _p.length) > 0 ? ((_q = transcriptsMessagesDetailsData === null || transcriptsMessagesDetailsData === void 0 ? void 0 : transcriptsMessagesDetailsData.messages) === null || _q === void 0 ? void 0 : _q.map((message, index) => {
233814
233871
  var _a, _b;
233815
- return (jsxs(React__default.Fragment, { children: [((message === null || message === void 0 ? void 0 : message.human) || !!((_a = message === null || message === void 0 ? void 0 : message.human_files) === null || _a === void 0 ? void 0 : _a.length)) && (jsxs("div", { className: "p-3 rounded-lg bg-blue-100 ml-4", children: [jsx("div", { className: "flex justify-between items-start mb-1", children: jsx("span", { className: "font-medium text-sm", children: t('analyticsAnalyticsTranscriptsStats.roleUser') }) }), message.human && (jsx("p", { className: "text-sm", children: getMessageContent(message.human) })), jsx(TranscriptFiles, { files: message.human_files })] })), ((message === null || message === void 0 ? void 0 : message.ai) || !!((_b = message === null || message === void 0 ? void 0 : message.ai_files) === null || _b === void 0 ? void 0 : _b.length)) && (jsxs("div", { className: "p-3 rounded-lg bg-white mr-4 border", style: { borderColor: 'oklch(.922 0 0)' }, children: [jsx("div", { className: "flex justify-between items-start mb-1", children: jsx("span", { className: "font-medium text-sm", children: t('analyticsAnalyticsTranscriptsStats.roleAgent') }) }), message.ai && jsx(Markdown, { children: getMessageContent(message.ai) }), jsx(TranscriptFiles, { files: message.ai_files }), jsx(TranscriptTurnDetails, { turn: message, ...turnDetails })] }))] }, index));
233872
+ return (jsxs(React__default.Fragment, { children: [((message === null || message === void 0 ? void 0 : message.human) || !!((_a = message === null || message === void 0 ? void 0 : message.human_files) === null || _a === void 0 ? void 0 : _a.length)) && (jsxs("div", { className: "p-3 rounded-lg bg-blue-100 ml-4", children: [jsx("div", { className: "flex justify-between items-start mb-1", children: jsx("span", { className: "font-medium text-sm", children: t('analyticsAnalyticsTranscriptsStats.roleUser') }) }), message.human && (jsx("p", { className: "text-sm", children: getMessageContent(message.human) })), jsx(TranscriptFiles, { files: message.human_files })] })), ((message === null || message === void 0 ? void 0 : message.ai) || !!((_b = message === null || message === void 0 ? void 0 : message.ai_files) === null || _b === void 0 ? void 0 : _b.length)) && (jsxs("div", { className: "p-3 rounded-lg bg-white mr-4 border", style: { borderColor: 'oklch(.922 0 0)' }, children: [jsx("div", { className: "flex justify-between items-start mb-1", children: jsx("span", { className: "font-medium text-sm", children: t('analyticsAnalyticsTranscriptsStats.roleAgent') }) }), message.ai && jsx(Markdown, { children: getMessageContent(message.ai) }), jsx(TranscriptFiles, { files: message.ai_files }), jsx(TranscriptTurnDetails, { turn: message, tenantKey: tenantKey, mentorId: detailMentorId, ...turnDetails })] }))] }, index));
233816
233873
  })) : (jsx("div", { className: "text-center text-gray-500 py-8", children: jsx("p", { children: t('analyticsAnalyticsTranscriptsStats.noMessages') }) })) })] })) })] }) }))] }));
233817
233874
  }
233818
233875
 
@@ -236507,7 +236564,10 @@ function AgentAuditLogView({ tenantKey, mentorId, userId, selectedMentorId, curr
236507
236564
  }, numberOfMonths: 2 }) })] }), jsx("div", { className: "min-w-[160px]", children: jsxs(Select$1, { value: actionFilter !== undefined ? String(actionFilter) : 'all', onValueChange: (val) => {
236508
236565
  setActionFilter(val === 'all' ? undefined : Number(val));
236509
236566
  setPage(1);
236510
- }, children: [jsx(SelectTrigger, { className: "w-full font-normal bg-white h-9", children: jsx(SelectValue, { placeholder: t('analyticsAnalyticsAuditLogStats.allActions') }) }), jsx(SelectContent, { children: ACTION_FILTER_OPTIONS.map((opt) => (jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value))) })] }) })] }) }), jsx("div", { className: "overflow-hidden rounded-md border border-[oklch(0.922_0_0)] bg-white", children: jsx("div", { className: "overflow-x-auto", children: jsx("div", { className: "inline-block min-w-full align-middle", children: isLoading ? (jsx("div", { className: "flex items-center justify-center w-full py-10", children: jsx("div", { className: "h-6 w-6 animate-spin rounded-full border-2 border-blue-600 border-t-transparent" }) })) : error && !is403 ? (jsx("div", { className: "py-8 text-center text-[#646464]", children: t('analyticsAnalyticsAuditLogStats.loadError') })) : !((_a = data === null || data === void 0 ? void 0 : data.results) === null || _a === void 0 ? void 0 : _a.length) ? (jsx("div", { className: "h-[120px]", children: jsx(EmptyStats, { title: t('analyticsAnalyticsAuditLogStats.noEntries') }) })) : (jsxs(Table, { className: "min-w-full", children: [jsx(TableHeader, { children: jsxs(TableRow, { className: "bg-muted/50 border-b border-[oklch(0.922_0_0)]", children: [jsx(TableHead, { className: "p-3 text-left text-sm whitespace-nowrap text-[#646464]", children: t('analyticsAnalyticsAuditLogStats.colUser') }), jsx(TableHead, { className: "p-3 text-left text-sm whitespace-nowrap text-[#646464]", children: t('analyticsAnalyticsAuditLogStats.colAction') }), jsx(TableHead, { className: "p-3 text-left text-sm whitespace-nowrap text-[#646464]", children: t('analyticsAnalyticsAuditLogStats.colTime') })] }) }), jsx(TableBody, { children: data.results.map((entry) => (jsxs(TableRow, { className: "border-b border-[oklch(0.922_0_0)] last:border-0", children: [jsx(TableCell, { className: "p-3 font-medium whitespace-nowrap text-[#646464]", children: jsx(UserProfileLink, { tenantKey: tenantKey, currentSPA: currentSPA, username: entry.actor_username, display: entry.actor_email || entry.actor_username }) }), jsx(TableCell, { className: "min-w-[250px] p-3 text-[#646464]", children: summarizeChanges(entry) }), jsx(TableCell, { className: "p-3 whitespace-nowrap text-[#646464]", children: formatDistanceToNow(new Date(entry.timestamp), { addSuffix: true }) })] }, entry.id))) })] })) }) }) }), jsx(IblPagination, { currentPage: page, totalPages: totalPages, onPageChange: setPage, disabled: isLoading })] }));
236567
+ }, children: [jsx(SelectTrigger, { className: "w-full font-normal bg-white h-9", children: jsx(SelectValue, { placeholder: t('analyticsAnalyticsAuditLogStats.allActions') }) }), jsx(SelectContent, { children: ACTION_FILTER_OPTIONS.map((opt) => (jsx(SelectItem, { value: opt.value, children: opt.label }, opt.value))) })] }) })] }) }), jsx("div", { className: "overflow-hidden rounded-md border border-[oklch(0.922_0_0)] bg-white", children: jsx("div", { className: "overflow-x-auto", children: jsx("div", { className: "inline-block min-w-full align-middle", children: isLoading ? (jsx("div", { className: "flex items-center justify-center w-full py-10", children: jsx("div", { className: "h-6 w-6 animate-spin rounded-full border-2 border-blue-600 border-t-transparent" }) })) : error && !is403 ? (jsx("div", { className: "py-8 text-center text-[#646464]", children: t('analyticsAnalyticsAuditLogStats.loadError') })) : !((_a = data === null || data === void 0 ? void 0 : data.results) === null || _a === void 0 ? void 0 : _a.length) ? (jsx("div", { className: "h-[120px]", children: jsx(EmptyStats, { title: t('analyticsAnalyticsAuditLogStats.noEntries') }) })) : (jsxs(Table, { className: "min-w-full", children: [jsx(TableHeader, { children: jsxs(TableRow, { className: "bg-muted/50 border-b border-[oklch(0.922_0_0)]", children: [jsx(TableHead, { className: "p-3 text-left text-sm whitespace-nowrap text-[#646464]", children: t('analyticsAnalyticsAuditLogStats.colUser') }), jsx(TableHead, { className: "p-3 text-left text-sm whitespace-nowrap text-[#646464]", children: t('analyticsAnalyticsAuditLogStats.colAction') }), jsx(TableHead, { className: "p-3 text-left text-sm whitespace-nowrap text-[#646464]", children: t('analyticsAnalyticsAuditLogStats.colTime') })] }) }), jsx(TableBody, { children: data.results.map((entry) => (jsxs(TableRow, { className: "border-b border-[oklch(0.922_0_0)] last:border-0", children: [jsx(TableCell, { className: "p-3 font-medium whitespace-nowrap text-[#646464]", children: jsx(UserProfileLink, { tenantKey: tenantKey, currentSPA: currentSPA, username: entry.actor_username, display: resolveUserIdentity({
236568
+ email: entry.actor_email,
236569
+ username: entry.actor_username,
236570
+ }).label }) }), jsx(TableCell, { className: "min-w-[250px] p-3 text-[#646464]", children: summarizeChanges(entry) }), jsx(TableCell, { className: "p-3 whitespace-nowrap text-[#646464]", children: formatDistanceToNow(new Date(entry.timestamp), { addSuffix: true }) })] }, entry.id))) })] })) }) }) }), jsx(IblPagination, { currentPage: page, totalPages: totalPages, onPageChange: setPage, disabled: isLoading })] }));
236511
236571
  }
236512
236572
  function AnalyticsAuditLogStats({ tenantKey, currentSPA, mentorId, userId, selectedMentorId, defaultScope = 'agent', timezone, canViewPlatformAudit, }) {
236513
236573
  const t = useT();
@@ -236554,7 +236614,7 @@ function toRowFromMentorMemory(memory) {
236554
236614
  content: memory.content,
236555
236615
  categoryName: (_b = (_a = memory.category) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : null,
236556
236616
  categorySlug: (_d = (_c = memory.category) === null || _c === void 0 ? void 0 : _c.slug) !== null && _d !== void 0 ? _d : null,
236557
- learner: memory.email || memory.username,
236617
+ learner: resolveUserIdentity(memory).label,
236558
236618
  username: memory.username,
236559
236619
  email: memory.email || null,
236560
236620
  mentorId: (_e = memory.mentor_id) !== null && _e !== void 0 ? _e : null,
@@ -236704,7 +236764,7 @@ const useMemory = ({ tenantKey, userId, mentorId, myMemory = false, }) => {
236704
236764
  let totalMemories = 0;
236705
236765
  const categories = groups.map((group) => {
236706
236766
  group.memories.forEach((memory) => {
236707
- learners.add(memory.email || memory.username);
236767
+ learners.add(resolveUserIdentity(memory).label);
236708
236768
  if (memory.is_auto_generated)
236709
236769
  autoCaptured += 1;
236710
236770
  });
@@ -248212,5 +248272,5 @@ var trainOrDeleteModal = /*#__PURE__*/Object.freeze({
248212
248272
  TrainOrDeleteModal: TrainOrDeleteModal
248213
248273
  });
248214
248274
 
248215
- export { ACCESS_COURSE_LABEL, ALL_CATEGORIES, AccessTimeHeatmap, AccessiblePaginate, AddMentorToProjectModal, AddSkillDialog, Admin, AdvancedTab, AgentConfigPrompts, AgentScopeLink, AgentSkills, AlertsTab, AnalyticsAgentPicker, AnalyticsAreaChart, AnalyticsAuditLogStats, AnalyticsBarChart, AnalyticsControlBar, AnalyticsCourseDetail, AnalyticsCourses, AnalyticsFinancialStats, AnalyticsGlobalFiltersProvider, AnalyticsLayout, AnalyticsLlmUsageStats, AnalyticsMemoryStats, AnalyticsMonetizationStats, AnalyticsNotice, AnalyticsOverview, AnalyticsPlatformAuditLog, AnalyticsProgramDetail, AnalyticsPrograms, AnalyticsReportDownload, AnalyticsReports, AnalyticsScopeProvider, AnalyticsSettingsProvider, AnalyticsTopicsStats, AnalyticsTraces, AnalyticsTranscriptsStats, AnalyticsUsersStats, AppleRestrictionModal, ApplyWizard, AuditLogChangesView, AuditLogJsonView, BUY_NOW_LABEL, BillingTab, CATALOGS, CHART_ORDINAL_RAMP, CHART_SERIES_COLORS, CHART_SINGLE_SERIES_COLOR, CREATE_AGENT_LABELS, CUA_DRIVER_REPO_URL, CUA_DRIVER_TAURI_COMMANDS, CUA_DRIVER_TAURI_EVENTS, CardInfoIcon, CategorizedDropdownMenu, ChartCardWrapper, ChartFiltersProvider, ChartLoading, ChartTooltipCard, ChatContext, ChatPrivacyToggle, ChatProvider, CompanyDialog, ConnectorManagementDialog, CopyButtonIcon, CourseAccessGuard, CourseCardSkeleton, CourseContentLoading, CourseOutline, CourseOutlineContext, CourseOutlineDrawer, CreateAgentForm, CreateAgentModal, CreateProjectModal, CreateWorkflowModal, CredentialBox, CredentialMiniBoxSkeleton, CredentialsList, CreditBalance, CustomDateRangePicker, CustomTooltip, DEFAULT_COWORK_REQUIRED_SIZE_GB, DEFAULT_LOCALE, DEFAULT_USER_IDENTITY_ORDER, DefaultEmptyBox, DeleteProjectModal, DeleteWorkflowModal, DiscoverContentCard, DiscoverFacetsFilter, DiscoverFilterDrawer, DocumentSidebar, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, ENROLL_NOW_COURSE_STARTING_SOON_LABEL, ENROLL_NOW_LABEL, EditAlertDialog, EducationBox, EducationDialog, EducationTab, EdxIframeContext, EmptyStats, ExperienceBox, ExperienceDialog, ExperienceTab, FacetFilterContext, Footer, GradebookTab, GroupsFilterDropdown, INVITATION_ONLY_LABEL, InstitutionDialog, IntegrationsTab, InviteUserContent, InviteUserDialog, InvitedUsersDialog, LOCAL_LLM_CHANGED_EVENT, LOCAL_MODELS, LlmModelBadge, Loader, LocalLLMTab, LocalModelsContent, LoginButton, MACOS_PERMISSIONS_COMMANDS, MEMORY_PAGE_SIZE, Markdown, MemoryAdminTab, MinimumMentorAlert, MonetizationTab, NotificationDisplay, NotificationDropdown, ONBOARDING_FIELD_TYPES, ONBOARDING_SECTORS, OnboardingField, OnboardingShell, OnboardingWizard, PLATFORM_AUDIT_DEEP_PAGING_THRESHOLD, PLATFORM_AUDIT_PAGE_SIZE, PaywallModal, PlacementExam, PlacementExamsDropdown, PlatformAuditLogDetailSheet, PlatformNavbar, PlatformNavbarSearch, Profile, ProfileTimeChart, ProgressDots, ProjectActionButtons, ProjectFilesModal, ProjectInstructionsModal, ProjectLandingPage, ProjectMentorsList, ProjectsPage, REQUEST_ACCESS_COURSE_STARTING_SOON_LABEL, REQUEST_ACCESS_LABEL, RankedBars, RenameProjectModal, ResumeBox, ResumeTab, RetrievedDocumentsButton, RichTextEditor, SERVICE_COLOR_ORDER, SUPPORTED_LOCALES, SandboxConfig, SearchableMultiSelect, SendNotificationDialog, SignupButton, SkeletonActivityStatBox, SkeletonAddSkillsLoading, SkeletonCreatePathwaySearchList, SkeletonDiscoverFilterBox, SkeletonEducationBox, SkeletonMultiplier, SkeletonPathwayBox, SkeletonProfileInfoCard, SkeletonSkillBox, SkillBox, SkillDetailModal, SkillLeaderboardChart, SkillsBox, SkillsList, SlashSkillPicker, SourceChip, Spinner, StatCard, StepHeader, SummerNoteTextEditor, TAURI_COMMANDS, TAURI_EVENTS, TRACES_PAGE_SIZE, TenantSwitcher, TimeFilter, TimeTrackingProvider, TimedExam, ToolCallIndicator, ToolDialogs, TopBanner, TranscriptFileCards, TranscriptRollupBadges, TranscriptTurnDetails, TrendSparkline, USER_ONBOARDING_FORM_SLUG, USER_ONBOARDING_FORM_VERSION, USER_ONBOARDING_SUBMISSION_SLUG, UpgradePackageModal, UserAvatar, UserOnboardingWizard, UserProfileDialog, UserProfileLink, Version, WebContainersI18nProvider, WorkflowSidebar, addBookmarksTab, areChartFiltersEqual, availablePlacementExams, buildOnboardingAgentPath, buildUserOnboardingFormMetadata, buildUserOnboardingSubmissionMetadata, coerceOnboardingAgent, coerceOnboardingAgents, coerceUserOnboardingForm, colorForRank, colorForSeries, conversationDocuments, emptyUserOnboardingForm, findLastResumeBlock, findOnboardingAgent, findSequentialParent, flattenVerticalBlocks, formatAuditTimestamp, formatCompactNumber, formatFileSize, formatLatencySeconds, formatResult, formatTimeZoneLabel, getCatalog, getFirstAvailableUnit, getFriendlyToolName, getLLMModelDisplayName, getLocalLLMModel, getLocalLLMToolSupport, getNextUnitIframe, getOrg, getParentBlockById, getParentsInfosFromSublessonId, getPreviousUnitIframe, getQueryLabel, getRandomCourseImage, getSectorById, getTenant, getUnitToIframe, getUserId, getUserName, granularityForFilter, hasOnboardingContent, humanizeAuditToken, humanizeFieldName, inBrowserPrint, inIframe, inferProviderKeysFromModel, initialCuaDriverInstallState, initialModelDownloadState, isCoworkEnabled, isEmailLike, isFileReference, isLlmUsageForbidden, isLlmUsageUnavailable, isLocalLLMEnabled, isPlatformNavbarCreditBalanceVisible, isSlashCommandToken, isTauriApp, isValidLocale, isValidTimeZone, llmUsageDateArgs, components as markdownComponents, modelSupportsCowork, onboardingPrimaryButtonClass, onboardingSecondaryButtonClass, readUserOnboardingForm, readUserOnboardingSubmission, readUserOnboardingSubmissions, resolveAuditTimeZone, resolveCreateAgentLabels, resolveUserIdentity, sanitizeCss, setCoworkEnabled, setLocalLLMEnabled, setLocalLLMModel, setLocalLLMToolSupport, smallestCoworkModel, summarizeTranscriptTurns, toFileReference, toRetrievedDocuments, toToolCallInfos, useAnalyticsGlobalFilters, useAnalyticsScope, useAnalyticsSettings, useAuditLog, useCatalogSearch, useChartFilters, useChatPrivacy, useChatState, useCourseDetail, useCourseMetadata, useCourses, useCuaDriver, useDiscover, useEdxIframe, useFinancial, useIframeMessageHandler, useLlmDisplayName, useLlmProviderCatalogue, useLlmTraces, useLlmUsage, useLocalStorage, useMemory, useModelDownload, useMonetization, useOverview, usePlatformAuditLabels, usePlatformAuditLog, useProfileActivityStats, useProfileCredentials, useProfilePathways, useProfilePrograms, useProfileSkills, useProfileTimeSpent, usePrograms, useReports, useScopedMentorDbId, useScopedMentorId, useSelectedLocalModel, useSlashSkillPicker, useSlashSkills, useT, useTauri, useTimeTracking, useTopics, useTranscripts, useUserCourses, useUserMetadata, useUsers, useWebContainersI18n, useWebContainersLocale };
248275
+ export { ACCESS_COURSE_LABEL, ALL_CATEGORIES, AccessTimeHeatmap, AccessiblePaginate, AddMentorToProjectModal, AddSkillDialog, Admin, AdvancedTab, AgentConfigPrompts, AgentScopeLink, AgentSkills, AlertsTab, AnalyticsAgentPicker, AnalyticsAreaChart, AnalyticsAuditLogStats, AnalyticsBarChart, AnalyticsControlBar, AnalyticsCourseDetail, AnalyticsCourses, AnalyticsFinancialStats, AnalyticsGlobalFiltersProvider, AnalyticsLayout, AnalyticsLlmUsageStats, AnalyticsMemoryStats, AnalyticsMonetizationStats, AnalyticsNotice, AnalyticsOverview, AnalyticsPlatformAuditLog, AnalyticsProgramDetail, AnalyticsPrograms, AnalyticsReportDownload, AnalyticsReports, AnalyticsScopeProvider, AnalyticsSettingsProvider, AnalyticsTopicsStats, AnalyticsTraces, AnalyticsTranscriptsStats, AnalyticsUsersStats, AppleRestrictionModal, ApplyWizard, AuditLogChangesView, AuditLogJsonView, BUY_NOW_LABEL, BillingTab, CATALOGS, CHART_ORDINAL_RAMP, CHART_SERIES_COLORS, CHART_SINGLE_SERIES_COLOR, CREATE_AGENT_LABELS, CUA_DRIVER_REPO_URL, CUA_DRIVER_TAURI_COMMANDS, CUA_DRIVER_TAURI_EVENTS, CardInfoIcon, CategorizedDropdownMenu, ChartCardWrapper, ChartFiltersProvider, ChartLoading, ChartTooltipCard, ChatContext, ChatPrivacyToggle, ChatProvider, CompanyDialog, ConnectorManagementDialog, CopyButtonIcon, CourseAccessGuard, CourseCardSkeleton, CourseContentLoading, CourseOutline, CourseOutlineContext, CourseOutlineDrawer, CreateAgentForm, CreateAgentModal, CreateProjectModal, CreateWorkflowModal, CredentialBox, CredentialMiniBoxSkeleton, CredentialsList, CreditBalance, CustomDateRangePicker, CustomTooltip, DEFAULT_COWORK_REQUIRED_SIZE_GB, DEFAULT_LOCALE, DEFAULT_USER_IDENTITY_ORDER, DefaultEmptyBox, DeleteProjectModal, DeleteWorkflowModal, DiscoverContentCard, DiscoverFacetsFilter, DiscoverFilterDrawer, DocumentSidebar, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, ENROLL_NOW_COURSE_STARTING_SOON_LABEL, ENROLL_NOW_LABEL, EditAlertDialog, EducationBox, EducationDialog, EducationTab, EdxIframeContext, EmptyStats, ExperienceBox, ExperienceDialog, ExperienceTab, FacetFilterContext, Footer, GradebookTab, GroupsFilterDropdown, INVITATION_ONLY_LABEL, InstitutionDialog, IntegrationsTab, InviteUserContent, InviteUserDialog, InvitedUsersDialog, LOCAL_LLM_CHANGED_EVENT, LOCAL_MODELS, LlmModelBadge, Loader, LocalLLMTab, LocalModelsContent, LoginButton, MACOS_PERMISSIONS_COMMANDS, MEMORY_PAGE_SIZE, Markdown, MemoryAdminTab, MinimumMentorAlert, MonetizationTab, NotificationDisplay, NotificationDropdown, ONBOARDING_FIELD_TYPES, ONBOARDING_SECTORS, OnboardingField, OnboardingShell, OnboardingWizard, PLATFORM_AUDIT_DEEP_PAGING_THRESHOLD, PLATFORM_AUDIT_PAGE_SIZE, PaywallModal, PlacementExam, PlacementExamsDropdown, PlatformAuditLogDetailSheet, PlatformNavbar, PlatformNavbarSearch, Profile, ProfileTimeChart, ProgressDots, ProjectActionButtons, ProjectFilesModal, ProjectInstructionsModal, ProjectLandingPage, ProjectMentorsList, ProjectsPage, REQUEST_ACCESS_COURSE_STARTING_SOON_LABEL, REQUEST_ACCESS_LABEL, RankedBars, RenameProjectModal, ResumeBox, ResumeTab, RetrievedDocumentsButton, RichTextEditor, SERVICE_COLOR_ORDER, SUPPORTED_LOCALES, SandboxConfig, SearchableMultiSelect, SendNotificationDialog, SignupButton, SkeletonActivityStatBox, SkeletonAddSkillsLoading, SkeletonCreatePathwaySearchList, SkeletonDiscoverFilterBox, SkeletonEducationBox, SkeletonMultiplier, SkeletonPathwayBox, SkeletonProfileInfoCard, SkeletonSkillBox, SkillBox, SkillDetailModal, SkillLeaderboardChart, SkillsBox, SkillsList, SlashSkillPicker, SourceChip, Spinner, StatCard, StepHeader, SummerNoteTextEditor, TAURI_COMMANDS, TAURI_EVENTS, TRACES_PAGE_SIZE, TenantSwitcher, TimeFilter, TimeTrackingProvider, TimedExam, ToolCallIndicator, ToolDialogs, TopBanner, TranscriptFileCards, TranscriptRollupBadges, TranscriptTurnDetails, TrendSparkline, USER_ONBOARDING_FORM_SLUG, USER_ONBOARDING_FORM_VERSION, USER_ONBOARDING_SUBMISSION_SLUG, UpgradePackageModal, UserAvatar, UserOnboardingWizard, UserProfileDialog, UserProfileLink, Version, WebContainersI18nProvider, WorkflowSidebar, addBookmarksTab, areChartFiltersEqual, availablePlacementExams, buildOnboardingAgentPath, buildUserOnboardingFormMetadata, buildUserOnboardingSubmissionMetadata, coerceOnboardingAgent, coerceOnboardingAgents, coerceUserOnboardingForm, colorForRank, colorForSeries, conversationDocuments, emptyUserOnboardingForm, findLastResumeBlock, findOnboardingAgent, findSequentialParent, flattenVerticalBlocks, formatAuditTimestamp, formatCompactNumber, formatFileSize, formatLatencySeconds, formatResult, formatTimeZoneLabel, getCatalog, getFirstAvailableUnit, getFriendlyToolName, getLLMModelDisplayName, getLocalLLMModel, getLocalLLMToolSupport, getNextUnitIframe, getOrg, getParentBlockById, getParentsInfosFromSublessonId, getPreviousUnitIframe, getQueryLabel, getRandomCourseImage, getSectorById, getTenant, getUnitToIframe, getUserId, getUserName, granularityForFilter, hasDistinctName, hasOnboardingContent, humanizeAuditToken, humanizeFieldName, inBrowserPrint, inIframe, inferProviderKeysFromModel, initialCuaDriverInstallState, initialModelDownloadState, isCoworkEnabled, isEmailLike, isFileReference, isLlmUsageForbidden, isLlmUsageUnavailable, isLocalLLMEnabled, isPlatformNavbarCreditBalanceVisible, isSlashCommandToken, isTauriApp, isValidLocale, isValidTimeZone, llmUsageDateArgs, components as markdownComponents, modelSupportsCowork, onboardingPrimaryButtonClass, onboardingSecondaryButtonClass, readUserOnboardingForm, readUserOnboardingSubmission, readUserOnboardingSubmissions, resolveAuditTimeZone, resolveCreateAgentLabels, resolveUserIdentity, sanitizeCss, setCoworkEnabled, setLocalLLMEnabled, setLocalLLMModel, setLocalLLMToolSupport, smallestCoworkModel, summarizeTranscriptTurns, toFileReference, toRetrievedDocuments, toToolCallInfos, useAnalyticsGlobalFilters, useAnalyticsScope, useAnalyticsSettings, useAuditLog, useCatalogSearch, useChartFilters, useChatPrivacy, useChatState, useCourseDetail, useCourseMetadata, useCourses, useCuaDriver, useDiscover, useEdxIframe, useFinancial, useIframeMessageHandler, useLlmDisplayName, useLlmProviderCatalogue, useLlmTraces, useLlmUsage, useLocalStorage, useMemory, useModelDownload, useMonetization, useOverview, usePlatformAuditLabels, usePlatformAuditLog, useProfileActivityStats, useProfileCredentials, useProfilePathways, useProfilePrograms, useProfileSkills, useProfileTimeSpent, usePrograms, useReports, useScopedMentorDbId, useScopedMentorId, useSelectedLocalModel, useSlashSkillPicker, useSlashSkills, useT, useTauri, useTimeTracking, useTopics, useTranscripts, useUserCourses, useUserMetadata, useUsers, useWebContainersI18n, useWebContainersLocale };
248216
248276
  //# sourceMappingURL=index.esm.js.map
@@ -221826,7 +221826,7 @@ function RetrievedDocumentsButton({ sessionId = '', documents, className, label,
221826
221826
  // Safe inside clickable transcript rows: the row must not react.
221827
221827
  event.stopPropagation();
221828
221828
  setIsOpen(true);
221829
- }, "data-testid": "retrieved-documents-button", children: [jsx(FileText, { className: "h-4 w-4 text-blue-600" }), jsx("span", { children: label !== null && label !== void 0 ? label : t('retrievedDocumentsButton.title') })] }), jsx(Dialog, { open: isOpen, onOpenChange: setIsOpen, children: jsxs(DialogContent, { className: "flex max-h-[80vh] flex-col gap-0 p-0 sm:max-w-[425px] md:max-w-[600px]", children: [jsx(DialogHeader, { className: "border-b px-4 py-2", children: jsxs(DialogTitle, { className: "flex items-center gap-2 text-base font-medium text-[#646464]", children: [jsx(FileText, { className: "h-5 w-5 text-blue-600" }), t('retrievedDocumentsButton.title')] }) }), jsx("div", { className: "min-h-0 flex-1 overflow-y-auto", "data-testid": "retrieved-documents-scroll", children: jsx(DocumentSidebar, { isModal: true, sessionId: sessionId, documents: documents }) })] }) })] }));
221829
+ }, "data-testid": "retrieved-documents-button", children: [jsx(FileText, { className: "h-4 w-4 text-blue-600" }), jsx("span", { children: label !== null && label !== void 0 ? label : t('retrievedDocumentsButton.title') })] }), jsx(Dialog, { open: isOpen, onOpenChange: setIsOpen, children: jsxs(DialogContent, { className: "flex max-h-[80vh] flex-col gap-0 p-0 sm:max-w-[425px] md:max-w-[600px]", children: [jsx(DialogHeader, { className: "border-b border-gray-200 px-4 py-2", children: jsxs(DialogTitle, { className: "flex items-center gap-2 text-base font-medium text-[#646464]", children: [jsx(FileText, { className: "h-5 w-5 text-blue-600" }), t('retrievedDocumentsButton.title')] }) }), jsx("div", { className: "min-h-0 flex-1 overflow-y-auto", "data-testid": "retrieved-documents-scroll", children: jsx(DocumentSidebar, { isModal: true, sessionId: sessionId, documents: documents }) })] }) })] }));
221830
221830
  }
221831
221831
 
221832
221832
  var COLLAPSIBLE_NAME = "Collapsible";
@@ -259773,6 +259773,154 @@ AnalyticsScopeContext.Provider;
259773
259773
 
259774
259774
  Array.from({ length: 24 }, (_, i) => i);
259775
259775
 
259776
+ /**
259777
+ * One place that decides how a user is shown.
259778
+ *
259779
+ * Backend payloads name their user in a dozen ways — `email` / `lti_email` /
259780
+ * `user_email`, `username` / `student` / `lti_username`, `name` /
259781
+ * `full_name` / `user_full_name`, or a nested `user` object — and most of
259782
+ * them can be empty, `null`, or a placeholder like "None". Every surface used
259783
+ * to pick through those on its own. `resolveUserIdentity()` takes whatever
259784
+ * the payload is and returns what to show, what to open a profile on, and
259785
+ * the avatar initial, so the rule lives here and can be tweaked once.
259786
+ *
259787
+ * Default rule: a real full name first (see {@link hasDistinctName}), else
259788
+ * email → username → the anonymous label.
259789
+ */
259790
+ const DEFAULT_USER_IDENTITY_ORDER = ['email', 'name', 'username'];
259791
+ /** Order used when the payload carries a real name (see {@link hasDistinctName}). */
259792
+ const NAME_FIRST_ORDER = ['name', 'email', 'username'];
259793
+ /**
259794
+ * Lower-case letters and digits of any script, accents and punctuation dropped,
259795
+ * so "Conrad Mugabe" / "conrad.mugabe" and "José" / "jose" compare equal.
259796
+ */
259797
+ const identityKey = (value) => (value !== null && value !== void 0 ? value : '')
259798
+ .normalize('NFKD')
259799
+ .replace(/[^\p{L}\p{N}]/gu, '')
259800
+ .toLowerCase();
259801
+ /**
259802
+ * Whether `name` is a real name rather than a restatement of the username.
259803
+ *
259804
+ * Many payloads fill the name field with the username ("conradmugabe" →
259805
+ * "Conrad Mugabe", "fayyaz_4_ibleducation_com"), which tells an admin nothing
259806
+ * the email does not. A name that matches none of the usernames on the record
259807
+ * is the most recognisable thing on it — including for LTI learners, whose
259808
+ * minted `<opaque-id>@edx-lti-1p3.com` address and username identify nobody.
259809
+ */
259810
+ function hasDistinctName(name, ...usernames) {
259811
+ const key = identityKey(name);
259812
+ if (!key)
259813
+ return false;
259814
+ return usernames.every((username) => key !== identityKey(username));
259815
+ }
259816
+ const DEFAULT_ANONYMOUS_LABEL = 'Anonymous';
259817
+ /** Values backends emit instead of "nothing". Compared case-insensitively. */
259818
+ const PLACEHOLDERS = new Set([
259819
+ '',
259820
+ 'none',
259821
+ 'null',
259822
+ 'undefined',
259823
+ 'n/a',
259824
+ 'na',
259825
+ '-',
259826
+ 'anonymous',
259827
+ 'anonymoususer',
259828
+ ]);
259829
+ /**
259830
+ * `local@domain.tld` with no whitespace, checked with a single pass rather than
259831
+ * a backtracking pattern (CodeQL flags `[^@]+@[^@]+\.[^@]+` as polynomial on
259832
+ * adversarial input).
259833
+ */
259834
+ const isEmailLike = (value) => {
259835
+ const text = value.trim();
259836
+ if (!text || /\s/.test(text))
259837
+ return false;
259838
+ const at = text.indexOf('@');
259839
+ if (at <= 0 || at !== text.lastIndexOf('@') || at === text.length - 1)
259840
+ return false;
259841
+ const domain = text.slice(at + 1);
259842
+ const dot = domain.indexOf('.');
259843
+ // A dot somewhere inside the domain, with something after the last one.
259844
+ return dot > 0 && !domain.endsWith('.');
259845
+ };
259846
+ /** Trimmed string, or undefined for blanks, non-strings and placeholders. */
259847
+ const clean = (value) => {
259848
+ if (typeof value !== 'string')
259849
+ return undefined;
259850
+ const trimmed = value.trim();
259851
+ return PLACEHOLDERS.has(trimmed.toLowerCase()) ? undefined : trimmed;
259852
+ };
259853
+ const firstOf = (...candidates) => {
259854
+ for (const candidate of candidates) {
259855
+ const value = clean(candidate);
259856
+ if (value)
259857
+ return value;
259858
+ }
259859
+ return undefined;
259860
+ };
259861
+ const nested = (source) => source.user && typeof source.user === 'object' ? source.user : undefined;
259862
+ /**
259863
+ * Resolve how a user should be displayed from any payload that names one.
259864
+ *
259865
+ * - `email`: `lti_email`, `email`, `user_email`, the nested `user`'s email —
259866
+ * or a username field that turns out to hold an email address.
259867
+ * - `username`: `lti_username`, `username`, `student`, `user_username`, the
259868
+ * nested `user`'s username, or a bare string/number `user` value.
259869
+ * - `name`: `user_full_name`, `full_name`, `name`, `first_name last_name`, or
259870
+ * the nested `user`'s name.
259871
+ * - `profileUsername`: the first *platform* username (never an LTI claim).
259872
+ *
259873
+ * A string source is treated as an email when it looks like one, else as a
259874
+ * username. Placeholder strings ("None", "-", "anonymous"…) count as absent.
259875
+ *
259876
+ * The default order flips to name → email → username when the record carries a
259877
+ * real name (see {@link hasDistinctName}): "Joseph Oti" is more use to an admin
259878
+ * than his address, while a name that merely repeats the username is not, so
259879
+ * those records keep showing the email. An explicit `order` is always obeyed.
259880
+ */
259881
+ function resolveUserIdentity(source, options = {}) {
259882
+ var _a, _b;
259883
+ const anonymousLabel = options.anonymousLabel || DEFAULT_ANONYMOUS_LABEL;
259884
+ const order = ((_a = options.order) === null || _a === void 0 ? void 0 : _a.length) ? options.order : DEFAULT_USER_IDENTITY_ORDER;
259885
+ const record = source && typeof source === 'object' ? source : { user: source !== null && source !== void 0 ? source : null };
259886
+ const inner = nested(record);
259887
+ // A bare `user` value is an id or username; numeric ids are still shown.
259888
+ const bareUser = typeof record.user === 'number' && Number.isFinite(record.user)
259889
+ ? String(record.user)
259890
+ : typeof record.user === 'string'
259891
+ ? clean(record.user)
259892
+ : undefined;
259893
+ let email = firstOf(record.lti_email, record.email, record.user_email, inner === null || inner === void 0 ? void 0 : inner.lti_email, inner === null || inner === void 0 ? void 0 : inner.email, inner === null || inner === void 0 ? void 0 : inner.user_email);
259894
+ const platformUsername = firstOf(record.username, record.student, record.user_username, inner === null || inner === void 0 ? void 0 : inner.username, inner === null || inner === void 0 ? void 0 : inner.student, inner === null || inner === void 0 ? void 0 : inner.user_username, bareUser && !isEmailLike(bareUser) ? bareUser : undefined);
259895
+ let username = firstOf(record.lti_username, inner === null || inner === void 0 ? void 0 : inner.lti_username, platformUsername);
259896
+ const composed = [clean(record.first_name), clean(record.last_name)].filter(Boolean).join(' ');
259897
+ const name = firstOf(record.user_full_name, record.full_name, record.name, composed, inner === null || inner === void 0 ? void 0 : inner.user_full_name, inner === null || inner === void 0 ? void 0 : inner.full_name, inner === null || inner === void 0 ? void 0 : inner.name);
259898
+ // A bare string that looks like an email is an email, and so is a
259899
+ // "username" field that actually carries one.
259900
+ if (!email && bareUser && isEmailLike(bareUser))
259901
+ email = bareUser;
259902
+ if (!email && username && isEmailLike(username)) {
259903
+ email = username;
259904
+ username = undefined;
259905
+ }
259906
+ const facts = { email, username, name };
259907
+ // Every username on the record counts: the platform one a profile opens on
259908
+ // and the LTI claim that would be displayed.
259909
+ const labelOrder = !((_b = options.order) === null || _b === void 0 ? void 0 : _b.length) && hasDistinctName(name, platformUsername, username)
259910
+ ? NAME_FIRST_ORDER
259911
+ : order;
259912
+ const label = labelOrder.map((field) => facts[field]).find(Boolean);
259913
+ return {
259914
+ label: label !== null && label !== void 0 ? label : anonymousLabel,
259915
+ email,
259916
+ username,
259917
+ name,
259918
+ profileUsername: platformUsername,
259919
+ initial: (label !== null && label !== void 0 ? label : anonymousLabel).charAt(0).toUpperCase() || 'A',
259920
+ isAnonymous: !label,
259921
+ };
259922
+ }
259923
+
259776
259924
  const asArray = (value) => (Array.isArray(value) ? value : []);
259777
259925
  const hasEntries = (value) => !!value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length > 0;
259778
259926
  /** Rollup counts as the conversation-list endpoint reports them: numbers, numeric strings or `null`. */
@@ -260084,7 +260232,7 @@ function SectionHeading({ icon, children }) {
260084
260232
  return (jsxs("h4", { className: "mb-2 flex items-center gap-1.5 text-xs font-semibold tracking-wide text-gray-700 uppercase", children: [icon, children] }));
260085
260233
  }
260086
260234
  const MODEL_KEYS = new Set(['llm_model', 'llm_provider']);
260087
- function MetadataSection({ metadata, renderModel, }) {
260235
+ function MetadataSection({ metadata, renderModel, tenantKey, mentorId, }) {
260088
260236
  var _a, _b;
260089
260237
  const t = useT();
260090
260238
  const labelFor = useFieldLabels();
@@ -260092,7 +260240,9 @@ function MetadataSection({ metadata, renderModel, }) {
260092
260240
  const model = ((_a = metadata.llm_model) !== null && _a !== void 0 ? _a : '').trim() || undefined;
260093
260241
  const provider = ((_b = metadata.llm_provider) !== null && _b !== void 0 ? _b : '').trim() || undefined;
260094
260242
  const hasModel = !!(model || provider);
260095
- return (jsxs("section", { "data-testid": "transcript-turn-metadata", children: [jsx(SectionHeading, { icon: jsx(Info$3, { className: "h-3.5 w-3.5", "aria-hidden": "true" }), children: t('analyticsTranscriptTurnDetails.metadata') }), hasModel && (jsx("div", { "data-testid": "transcript-turn-model", className: "mb-2", children: (renderModel !== null && renderModel !== void 0 ? renderModel : defaultRenderModel)({ model, provider }) })), jsx(DetailRows, { entries: rest, labelFor: labelFor })] }));
260243
+ return (jsxs("section", { "data-testid": "transcript-turn-metadata", children: [jsx(SectionHeading, { icon: jsx(Info$3, { className: "h-3.5 w-3.5", "aria-hidden": "true" }), children: t('analyticsTranscriptTurnDetails.metadata') }), hasModel && (jsx("div", { "data-testid": "transcript-turn-model", className: "mb-2", children: renderModel
260244
+ ? renderModel({ model, provider })
260245
+ : defaultRenderModel({ model, provider, tenantKey, mentorId }) })), jsx(DetailRows, { entries: rest, labelFor: labelFor })] }));
260096
260246
  }
260097
260247
  function RequestContextSection({ requestContext }) {
260098
260248
  const t = useT();
@@ -260107,7 +260257,7 @@ function RequestContextSection({ requestContext }) {
260107
260257
  */
260108
260258
  const defaultRenderDocuments = (documents) => (jsx(RetrievedDocumentsButton, { documents: toRetrievedDocuments(documents) }));
260109
260259
  const defaultRenderToolCalls = (toolCalls) => (jsx(ToolCallIndicator, { toolCalls: toToolCallInfos(toolCalls), showResults: true }));
260110
- const defaultRenderModel = ({ model, provider }) => (jsx(LlmModelBadge, { model: model, provider: provider }));
260260
+ const defaultRenderModel = ({ model, provider, tenantKey, mentorId, }) => jsx(LlmModelBadge, { model: model, provider: provider, tenantKey: tenantKey, mentorId: mentorId });
260111
260261
  const defaultRenderFiles = (files) => (jsx(TranscriptFileCards, { files: files }));
260112
260262
  /**
260113
260263
  * Collapsible "Sources / Tools / Metadata" panel for one AI turn of an admin
@@ -260117,7 +260267,7 @@ const defaultRenderFiles = (files) => (jsx(TranscriptFileCards, { files: files }
260117
260267
  * renders nothing at all when every field is null or empty, so it is safe to
260118
260268
  * drop under every AI bubble.
260119
260269
  */
260120
- function TranscriptTurnDetails({ turn, className, defaultOpen = false, renderDocuments, renderToolCalls, renderModel, renderFiles, }) {
260270
+ function TranscriptTurnDetails({ turn, className, defaultOpen = false, renderDocuments, renderToolCalls, renderModel, renderFiles, tenantKey, mentorId, }) {
260121
260271
  const t = useT();
260122
260272
  const labelFor = useFieldLabels();
260123
260273
  const [open, setOpen] = React__default.useState(defaultOpen);
@@ -260131,7 +260281,7 @@ function TranscriptTurnDetails({ turn, className, defaultOpen = false, renderDoc
260131
260281
  const requestContext = hasEntries(turn === null || turn === void 0 ? void 0 : turn.request_context) ? turn === null || turn === void 0 ? void 0 : turn.request_context : null;
260132
260282
  return (jsx(DetailCtx.Provider, { value: detailContext, children: jsxs(Collapsible, { open: open, onOpenChange: setOpen, className: cn('mt-2', className), "data-testid": "transcript-turn-details", children: [jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [jsx(CollapsibleTrigger, { asChild: true, children: jsxs(Button$1, { type: "button", variant: "outline", size: "sm", className: "flex items-center gap-2 border-gray-200 bg-white text-gray-700 hover:bg-gray-50", "aria-expanded": open, children: [jsx(ChevronDown, { className: cn('h-4 w-4 transition-transform', open && 'rotate-180'), "aria-hidden": "true" }), open
260133
260283
  ? t('analyticsTranscriptTurnDetails.hideDetails')
260134
- : t('analyticsTranscriptTurnDetails.showDetails')] }) }), documents.length > 0 && (jsx("div", { "data-testid": "transcript-turn-sources", className: "contents", children: (renderDocuments !== null && renderDocuments !== void 0 ? renderDocuments : defaultRenderDocuments)(documents) })), jsx(TranscriptRollupBadges, { documentsCount: 0, toolCallsCount: toolCalls.length })] }), jsxs(CollapsibleContent, { className: "mt-2 space-y-5 rounded-lg border border-gray-200 bg-white p-3", children: [toolCalls.length > 0 && (jsx("div", { "data-testid": "transcript-turn-tools", children: (renderToolCalls !== null && renderToolCalls !== void 0 ? renderToolCalls : defaultRenderToolCalls)(toolCalls) })), metadata && jsx(MetadataSection, { metadata: metadata, renderModel: renderModel }), requestContext && jsx(RequestContextSection, { requestContext: requestContext })] })] }) }));
260284
+ : t('analyticsTranscriptTurnDetails.showDetails')] }) }), documents.length > 0 && (jsx("div", { "data-testid": "transcript-turn-sources", className: "contents", children: (renderDocuments !== null && renderDocuments !== void 0 ? renderDocuments : defaultRenderDocuments)(documents) })), jsx(TranscriptRollupBadges, { documentsCount: 0, toolCallsCount: toolCalls.length })] }), jsxs(CollapsibleContent, { className: "mt-2 space-y-5 rounded-lg border border-gray-200 bg-white p-3", children: [toolCalls.length > 0 && (jsx("div", { "data-testid": "transcript-turn-tools", children: (renderToolCalls !== null && renderToolCalls !== void 0 ? renderToolCalls : defaultRenderToolCalls)(toolCalls) })), metadata && (jsx(MetadataSection, { metadata: metadata, renderModel: renderModel, tenantKey: tenantKey, mentorId: mentorId })), requestContext && jsx(RequestContextSection, { requestContext: requestContext })] })] }) }));
260135
260285
  }
260136
260286
  /**
260137
260287
  * "Documents · N" / "Tools · N" chips for a conversation row, from the
@@ -260147,118 +260297,6 @@ function TranscriptRollupBadges({ documentsCount, toolCallsCount, className, })
260147
260297
  return (jsxs("span", { className: cn('inline-flex flex-wrap items-center gap-1.5', className), "data-testid": "transcript-rollup-badges", children: [documents > 0 && (jsxs(Badge, { variant: "outline", className: "gap-1 border-transparent bg-blue-50 font-normal text-blue-700", "data-testid": "transcript-sources-badge", children: [jsx(FileText, { className: "h-3 w-3", "aria-hidden": "true" }), t('analyticsTranscriptTurnDetails.sourcesBadge', { count: documents })] })), tools > 0 && (jsxs(Badge, { variant: "outline", className: "gap-1 border-transparent bg-amber-50 font-normal text-amber-800", "data-testid": "transcript-tools-badge", children: [jsx(Wrench, { className: "h-3 w-3", "aria-hidden": "true" }), t('analyticsTranscriptTurnDetails.toolsBadge', { count: tools })] }))] }));
260148
260298
  }
260149
260299
 
260150
- /**
260151
- * One place that decides how a user is shown.
260152
- *
260153
- * Backend payloads name their user in a dozen ways — `email` / `lti_email` /
260154
- * `user_email`, `username` / `student` / `lti_username`, `name` /
260155
- * `full_name` / `user_full_name`, or a nested `user` object — and most of
260156
- * them can be empty, `null`, or a placeholder like "None". Every surface used
260157
- * to pick through those on its own. `resolveUserIdentity()` takes whatever
260158
- * the payload is and returns what to show, what to open a profile on, and
260159
- * the avatar initial, so the rule lives here and can be tweaked once.
260160
- *
260161
- * Default rule: email → full name → username → the anonymous label.
260162
- */
260163
- const DEFAULT_USER_IDENTITY_ORDER = ['email', 'name', 'username'];
260164
- const DEFAULT_ANONYMOUS_LABEL = 'Anonymous';
260165
- /** Values backends emit instead of "nothing". Compared case-insensitively. */
260166
- const PLACEHOLDERS = new Set([
260167
- '',
260168
- 'none',
260169
- 'null',
260170
- 'undefined',
260171
- 'n/a',
260172
- 'na',
260173
- '-',
260174
- 'anonymous',
260175
- 'anonymoususer',
260176
- ]);
260177
- /**
260178
- * `local@domain.tld` with no whitespace, checked with a single pass rather than
260179
- * a backtracking pattern (CodeQL flags `[^@]+@[^@]+\.[^@]+` as polynomial on
260180
- * adversarial input).
260181
- */
260182
- const isEmailLike = (value) => {
260183
- const text = value.trim();
260184
- if (!text || /\s/.test(text))
260185
- return false;
260186
- const at = text.indexOf('@');
260187
- if (at <= 0 || at !== text.lastIndexOf('@') || at === text.length - 1)
260188
- return false;
260189
- const domain = text.slice(at + 1);
260190
- const dot = domain.indexOf('.');
260191
- // A dot somewhere inside the domain, with something after the last one.
260192
- return dot > 0 && !domain.endsWith('.');
260193
- };
260194
- /** Trimmed string, or undefined for blanks, non-strings and placeholders. */
260195
- const clean = (value) => {
260196
- if (typeof value !== 'string')
260197
- return undefined;
260198
- const trimmed = value.trim();
260199
- return PLACEHOLDERS.has(trimmed.toLowerCase()) ? undefined : trimmed;
260200
- };
260201
- const firstOf = (...candidates) => {
260202
- for (const candidate of candidates) {
260203
- const value = clean(candidate);
260204
- if (value)
260205
- return value;
260206
- }
260207
- return undefined;
260208
- };
260209
- const nested = (source) => source.user && typeof source.user === 'object' ? source.user : undefined;
260210
- /**
260211
- * Resolve how a user should be displayed from any payload that names one.
260212
- *
260213
- * - `email`: `lti_email`, `email`, `user_email`, the nested `user`'s email —
260214
- * or a username field that turns out to hold an email address.
260215
- * - `username`: `lti_username`, `username`, `student`, `user_username`, the
260216
- * nested `user`'s username, or a bare string/number `user` value.
260217
- * - `name`: `user_full_name`, `full_name`, `name`, `first_name last_name`, or
260218
- * the nested `user`'s name.
260219
- * - `profileUsername`: the first *platform* username (never an LTI claim).
260220
- *
260221
- * A string source is treated as an email when it looks like one, else as a
260222
- * username. Placeholder strings ("None", "-", "anonymous"…) count as absent.
260223
- */
260224
- function resolveUserIdentity(source, options = {}) {
260225
- var _a;
260226
- const anonymousLabel = options.anonymousLabel || DEFAULT_ANONYMOUS_LABEL;
260227
- const order = ((_a = options.order) === null || _a === void 0 ? void 0 : _a.length) ? options.order : DEFAULT_USER_IDENTITY_ORDER;
260228
- const record = source && typeof source === 'object' ? source : { user: source !== null && source !== void 0 ? source : null };
260229
- const inner = nested(record);
260230
- // A bare `user` value is an id or username; numeric ids are still shown.
260231
- const bareUser = typeof record.user === 'number' && Number.isFinite(record.user)
260232
- ? String(record.user)
260233
- : typeof record.user === 'string'
260234
- ? clean(record.user)
260235
- : undefined;
260236
- let email = firstOf(record.lti_email, record.email, record.user_email, inner === null || inner === void 0 ? void 0 : inner.lti_email, inner === null || inner === void 0 ? void 0 : inner.email, inner === null || inner === void 0 ? void 0 : inner.user_email);
260237
- const platformUsername = firstOf(record.username, record.student, record.user_username, inner === null || inner === void 0 ? void 0 : inner.username, inner === null || inner === void 0 ? void 0 : inner.student, inner === null || inner === void 0 ? void 0 : inner.user_username, bareUser && !isEmailLike(bareUser) ? bareUser : undefined);
260238
- let username = firstOf(record.lti_username, inner === null || inner === void 0 ? void 0 : inner.lti_username, platformUsername);
260239
- const composed = [clean(record.first_name), clean(record.last_name)].filter(Boolean).join(' ');
260240
- const name = firstOf(record.user_full_name, record.full_name, record.name, composed, inner === null || inner === void 0 ? void 0 : inner.user_full_name, inner === null || inner === void 0 ? void 0 : inner.full_name, inner === null || inner === void 0 ? void 0 : inner.name);
260241
- // A bare string that looks like an email is an email, and so is a
260242
- // "username" field that actually carries one.
260243
- if (!email && bareUser && isEmailLike(bareUser))
260244
- email = bareUser;
260245
- if (!email && username && isEmailLike(username)) {
260246
- email = username;
260247
- username = undefined;
260248
- }
260249
- const facts = { email, username, name };
260250
- const label = order.map((field) => facts[field]).find(Boolean);
260251
- return {
260252
- label: label !== null && label !== void 0 ? label : anonymousLabel,
260253
- email,
260254
- username,
260255
- name,
260256
- profileUsername: platformUsername,
260257
- initial: (label !== null && label !== void 0 ? label : anonymousLabel).charAt(0).toUpperCase() || 'A',
260258
- isAnonymous: !label,
260259
- };
260260
- }
260261
-
260262
260300
  const AnalyticsSettingsContext = createContext({});
260263
260301
  AnalyticsSettingsContext.Provider;
260264
260302
 
@@ -279113,7 +279151,7 @@ function AgentHistoryTab({ renderMessageContent, PaginationComponent, onExport,
279113
279151
  // Use mentor summaries data for rating and tags
279114
279152
  const averageRating = (mentorSummaries === null || mentorSummaries === void 0 ? void 0 : mentorSummaries.rating) || 0;
279115
279153
  const topicTags = (mentorSummaries === null || mentorSummaries === void 0 ? void 0 : mentorSummaries.tags) || [];
279116
- // Who the previewed conversation belongs to (email → full name → username → Anonymous).
279154
+ // Who the previewed conversation belongs to (the shared identity rule).
279117
279155
  const identityOptions = { anonymousLabel: labels.conversation.anonymous };
279118
279156
  const selectedOwner = resolveUserIdentity(selectedConversation, identityOptions);
279119
279157
  const previewOwner = resolveUserIdentity(previewConversationContent, identityOptions);
@@ -279175,11 +279213,11 @@ function AgentHistoryTab({ renderMessageContent, PaginationComponent, onExport,
279175
279213
  jsx(RetrievedDocumentsButton, { documents: documents, label: labels.conversation.documentsChip(rollup.documentsCount), className: "h-6 gap-1 rounded-md border-transparent bg-blue-50 px-2 text-xs font-normal text-blue-700 hover:bg-blue-100 [&_svg]:h-3 [&_svg]:w-3" })), jsx(TranscriptRollupBadges, { documentsCount: 0, toolCallsCount: rollup.toolCallsCount })] }))] }), jsx("div", { className: "flex items-center gap-2 text-sm text-gray-600", children: jsx(ChevronRight, { className: "h-4 w-4" }) })] }, conversation.id));
279176
279214
  }) }), totalPages > 1 && PaginationComponent && (jsx("div", { className: "my-2", children: jsx(PaginationComponent, { currentPage: currentPage, totalPages: totalPages, onPageChange: handlePageChange, disabled: isChatHistoryFetching || isChatHistoryLoading, disableNumberedButtons: isMobile }) }))] }), jsx("div", { className: "scrollbar-hide hidden max-h-[500px] flex-col overflow-y-auto rounded-lg border bg-white p-6 shadow-sm md:flex", tabIndex: 0, role: "region", "aria-label": t('tabsHistoryTab.conversationPreviewLabel'), children: selectedConversation && previewConversationContent ? (jsxs(Fragment$1, { children: [jsxs("div", { className: "mb-4", children: [jsx("h3", { className: "mb-1 text-base font-semibold text-gray-700", children: ((_m = selectedConversation.messages[0]) === null || _m === void 0 ? void 0 : _m.human) || labels.conversation.title }), jsx("span", { className: "text-sm text-gray-500", children: format$1(new Date(selectedConversation.inserted_at), "MMM dd, yyyy 'at' h:mm a") }), jsx(RetrievedDocumentsButton, { sessionId: selectedConversation.id, documents: conversationDocuments(selectedConversation.messages).length > 0
279177
279215
  ? conversationDocuments(selectedConversation.messages)
279178
- : undefined, className: "mt-2" })] }), jsxs("div", { className: "space-y-6", children: [selectedConversation.messages.map((message, index) => (jsxs("div", { className: "space-y-4", children: [jsxs("div", { className: "flex items-start gap-3", children: [jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-blue-100", children: jsx("span", { className: "text-sm font-medium text-blue-600", children: selectedOwner.initial }) }), jsxs("div", { className: "flex-1", children: [jsx("div", { className: "font-medium text-gray-700", children: jsx(UserProfileLink, { tenantKey: tenantKey, username: selectedOwner.profileUsername, display: selectedOwner.label, currentSPA: currentSPA }) }), jsx("p", { className: "mt-1 text-sm whitespace-pre-line text-gray-500", children: message.human }), jsx(TranscriptFileCards, { files: attachmentCards(message.human_files) })] })] }), jsxs("div", { className: "flex items-start gap-3 border-t border-gray-200 pt-4", children: [jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-gray-100", children: jsx("span", { className: "text-sm font-medium text-gray-600", children: t('tabsHistoryTab.aiAvatarLabel') }) }), jsxs("div", { className: "flex-1", children: [jsx("div", { className: "font-medium text-gray-700", children: labels.conversation.aiAgent }), jsx("div", { className: "mt-1 text-sm text-gray-500", children: renderAiContent(message.ai) }), jsx(TranscriptFileCards, { files: attachmentCards(message.ai_files) }), jsx(TranscriptTurnDetails, { turn: message })] })] })] }, index))), (selectedConversation === null || selectedConversation === void 0 ? void 0 : selectedConversation.memory_tracked) && (jsxs("div", { className: "border-t border-gray-200 pt-4", children: [jsxs(Button$1, { variant: "outline", onClick: () => setShowConversationMemory(!showConversationMemory), className: "flex w-full items-center justify-center gap-2 border-gray-200 text-gray-700 hover:bg-gray-50", children: [jsx(Brain, { className: "h-4 w-4" }), showConversationMemory ? labels.memory.hide : labels.memory.show] }), showConversationMemory && (jsx("div", { className: "mt-4 space-y-6", children: isConversationMemoryLoading ? (jsx("div", { className: "flex items-center justify-center py-4", children: jsx(Spinner, {}) })) : ((_o = conversationMemory === null || conversationMemory === void 0 ? void 0 : conversationMemory.entries) === null || _o === void 0 ? void 0 : _o.length) ? (jsx(Fragment$1, { children: conversationMemory.entries.map((entry, index) => (jsxs("div", { children: [jsx("h4", { className: "mb-3 font-semibold text-gray-900", children: entry.key }), jsx("ul", { className: "space-y-2", children: jsx("li", { className: "flex items-start gap-2 text-sm text-gray-600", children: entry.value }) })] }, index))) })) : (jsx("div", { className: "text-sm text-gray-500", children: labels.memory.notAvailable })) }))] }))] })] })) : (jsx("div", { className: "flex h-full items-center justify-center text-gray-500", children: labels.conversation.selectPrompt })) })] }))] }) }), jsx(Dialog, { open: isConversationPreviewModalOpen, onOpenChange: setIsConversationPreviewModalOpen, children: jsxs(DialogContent, { className: "scrollbar-hide mx-auto max-h-[80vh] max-w-[90vw] overflow-x-hidden overflow-y-auto", children: [jsx(DialogHeader, { children: jsx(DialogTitle, { className: "text-lg font-semibold text-gray-900", children: previewConversationContent
279216
+ : undefined, className: "mt-2" })] }), jsxs("div", { className: "space-y-6", children: [selectedConversation.messages.map((message, index) => (jsxs("div", { className: "space-y-4", children: [jsxs("div", { className: "flex items-start gap-3", children: [jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-blue-100", children: jsx("span", { className: "text-sm font-medium text-blue-600", children: selectedOwner.initial }) }), jsxs("div", { className: "flex-1", children: [jsx("div", { className: "font-medium text-gray-700", children: jsx(UserProfileLink, { tenantKey: tenantKey, username: selectedOwner.profileUsername, display: selectedOwner.label, currentSPA: currentSPA }) }), jsx("p", { className: "mt-1 text-sm whitespace-pre-line text-gray-500", children: message.human }), jsx(TranscriptFileCards, { files: attachmentCards(message.human_files) })] })] }), jsxs("div", { className: "flex items-start gap-3 border-t border-gray-200 pt-4", children: [jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-gray-100", children: jsx("span", { className: "text-sm font-medium text-gray-600", children: t('tabsHistoryTab.aiAvatarLabel') }) }), jsxs("div", { className: "flex-1", children: [jsx("div", { className: "font-medium text-gray-700", children: labels.conversation.aiAgent }), jsx("div", { className: "mt-1 text-sm text-gray-500", children: renderAiContent(message.ai) }), jsx(TranscriptFileCards, { files: attachmentCards(message.ai_files) }), jsx(TranscriptTurnDetails, { turn: message, tenantKey: tenantKey, mentorId: mentorId })] })] })] }, index))), (selectedConversation === null || selectedConversation === void 0 ? void 0 : selectedConversation.memory_tracked) && (jsxs("div", { className: "border-t border-gray-200 pt-4", children: [jsxs(Button$1, { variant: "outline", onClick: () => setShowConversationMemory(!showConversationMemory), className: "flex w-full items-center justify-center gap-2 border-gray-200 text-gray-700 hover:bg-gray-50", children: [jsx(Brain, { className: "h-4 w-4" }), showConversationMemory ? labels.memory.hide : labels.memory.show] }), showConversationMemory && (jsx("div", { className: "mt-4 space-y-6", children: isConversationMemoryLoading ? (jsx("div", { className: "flex items-center justify-center py-4", children: jsx(Spinner, {}) })) : ((_o = conversationMemory === null || conversationMemory === void 0 ? void 0 : conversationMemory.entries) === null || _o === void 0 ? void 0 : _o.length) ? (jsx(Fragment$1, { children: conversationMemory.entries.map((entry, index) => (jsxs("div", { children: [jsx("h4", { className: "mb-3 font-semibold text-gray-900", children: entry.key }), jsx("ul", { className: "space-y-2", children: jsx("li", { className: "flex items-start gap-2 text-sm text-gray-600", children: entry.value }) })] }, index))) })) : (jsx("div", { className: "text-sm text-gray-500", children: labels.memory.notAvailable })) }))] }))] })] })) : (jsx("div", { className: "flex h-full items-center justify-center text-gray-500", children: labels.conversation.selectPrompt })) })] }))] }) }), jsx(Dialog, { open: isConversationPreviewModalOpen, onOpenChange: setIsConversationPreviewModalOpen, children: jsxs(DialogContent, { className: "scrollbar-hide mx-auto max-h-[80vh] max-w-[90vw] overflow-x-hidden overflow-y-auto", children: [jsx(DialogHeader, { children: jsx(DialogTitle, { className: "text-lg font-semibold text-gray-900", children: previewConversationContent
279179
279217
  ? ((_p = previewConversationContent.messages[0]) === null || _p === void 0 ? void 0 : _p.human) || labels.conversation.title
279180
279218
  : labels.conversation.title }) }), jsx("div", { className: "mx-auto max-w-full overflow-x-hidden", children: previewConversationContent && (jsxs("div", { className: "scrollbar-hide space-y-4 overflow-y-auto", children: [jsx("div", { className: "text-sm text-gray-500", children: format$1(new Date(previewConversationContent.inserted_at), "MMM dd, yyyy 'at' h:mm a") }), jsx(RetrievedDocumentsButton, { sessionId: previewConversationContent.id, documents: conversationDocuments(previewConversationContent.messages).length > 0
279181
279219
  ? conversationDocuments(previewConversationContent.messages)
279182
- : undefined }), previewConversationContent.messages.map((message, index) => (jsxs("div", { className: "space-y-4", children: [jsxs("div", { className: "flex items-start gap-3", children: [jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-blue-100", children: jsx("span", { className: "text-sm font-medium text-blue-600", children: previewOwner.initial }) }), jsxs("div", { className: "overflow-x-hidden", children: [jsx("div", { className: "truncate font-medium text-gray-900", children: jsx(UserProfileLink, { tenantKey: tenantKey, username: previewOwner.profileUsername, display: previewOwner.label, currentSPA: currentSPA }) }), jsx("p", { className: "mt-1 text-sm whitespace-pre-line text-gray-900", children: message.human }), jsx(TranscriptFileCards, { files: attachmentCards(message.human_files) })] })] }), jsxs("div", { className: "flex items-start gap-3 border-t border-gray-200 pt-4", children: [jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-gray-100", children: jsx("span", { className: "text-sm font-medium text-gray-600", children: t('tabsHistoryTab.aiAvatarLabel') }) }), jsxs("div", { className: "overflow-x-hidden", children: [jsx("div", { className: "font-medium text-gray-900", children: labels.conversation.aiAgent }), jsx("div", { className: "mt-1 text-sm text-gray-900", children: renderAiContent(message.ai) }), jsx(TranscriptFileCards, { files: attachmentCards(message.ai_files) }), jsx(TranscriptTurnDetails, { turn: message })] })] })] }, index)))] })) })] }) })] }));
279220
+ : undefined }), previewConversationContent.messages.map((message, index) => (jsxs("div", { className: "space-y-4", children: [jsxs("div", { className: "flex items-start gap-3", children: [jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-blue-100", children: jsx("span", { className: "text-sm font-medium text-blue-600", children: previewOwner.initial }) }), jsxs("div", { className: "overflow-x-hidden", children: [jsx("div", { className: "truncate font-medium text-gray-900", children: jsx(UserProfileLink, { tenantKey: tenantKey, username: previewOwner.profileUsername, display: previewOwner.label, currentSPA: currentSPA }) }), jsx("p", { className: "mt-1 text-sm whitespace-pre-line text-gray-900", children: message.human }), jsx(TranscriptFileCards, { files: attachmentCards(message.human_files) })] })] }), jsxs("div", { className: "flex items-start gap-3 border-t border-gray-200 pt-4", children: [jsx("div", { className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-gray-100", children: jsx("span", { className: "text-sm font-medium text-gray-600", children: t('tabsHistoryTab.aiAvatarLabel') }) }), jsxs("div", { className: "overflow-x-hidden", children: [jsx("div", { className: "font-medium text-gray-900", children: labels.conversation.aiAgent }), jsx("div", { className: "mt-1 text-sm text-gray-900", children: renderAiContent(message.ai) }), jsx(TranscriptFileCards, { files: attachmentCards(message.ai_files) }), jsx(TranscriptTurnDetails, { turn: message, tenantKey: tenantKey, mentorId: mentorId })] })] })] }, index)))] })) })] }) })] }));
279183
279221
  }
279184
279222
 
279185
279223
  const ANONYMOUS_USERNAME = 'anonymous';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iblai/iblai-js",
3
- "version": "2.18.0",
3
+ "version": "2.18.1",
4
4
  "description": "Unified JavaScript SDK for IBL.ai — re-exports data-layer, web-containers, and web-utils under a single package",
5
5
  "type": "module",
6
6
  "engines": {
@@ -76,10 +76,10 @@
76
76
  "axios": "1.13.6",
77
77
  "dotenv": "16.6.1",
78
78
  "winston": "3.19.0",
79
- "@iblai/data-layer": "1.19.0",
79
+ "@iblai/data-layer": "1.19.1",
80
80
  "@iblai/mcp": "1.16.0",
81
- "@iblai/web-containers": "1.25.0",
82
- "@iblai/web-utils": "2.4.8"
81
+ "@iblai/web-utils": "2.4.9",
82
+ "@iblai/web-containers": "1.25.1"
83
83
  },
84
84
  "peerDependencies": {
85
85
  "@iblai/iblai-api": "4.166.0-ai",