@carlonicora/nextjs-jsonapi 1.78.0 → 1.80.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/dist/{AssistantMessageInterface-DS_tyJTV.d.ts → AssistantMessageInterface-BpEhx2pC.d.ts} +19 -2
  2. package/dist/{AssistantMessageInterface-D0Kwf8CR.d.mts → AssistantMessageInterface-DJ3Me16Y.d.mts} +19 -2
  3. package/dist/{AuthComponent-Blbs06ud.d.ts → AuthComponent-B6DIk8Vf.d.ts} +1 -1
  4. package/dist/{AuthComponent-huIaK5rm.d.mts → AuthComponent-BKI0ZbtD.d.mts} +1 -1
  5. package/dist/{BlockNoteEditor-JXK3JGKJ.mjs → BlockNoteEditor-3M5PD3BZ.mjs} +4 -4
  6. package/dist/{BlockNoteEditor-2G5UYALC.js → BlockNoteEditor-YLTPJPTV.js} +14 -14
  7. package/dist/{BlockNoteEditor-2G5UYALC.js.map → BlockNoteEditor-YLTPJPTV.js.map} +1 -1
  8. package/dist/RbacTypes-BTbr27Ew.d.mts +43 -0
  9. package/dist/RbacTypes-BTbr27Ew.d.ts +43 -0
  10. package/dist/{auth.interface-CQJ6A2Cj.d.ts → auth.interface-BBUgMZzs.d.ts} +1 -1
  11. package/dist/{auth.interface-Bdq7-8iV.d.mts → auth.interface-XYEREOD6.d.mts} +1 -1
  12. package/dist/billing/index.js +346 -346
  13. package/dist/billing/index.mjs +3 -3
  14. package/dist/{chunk-ZEDB6JVB.js → chunk-4NOQNTFI.js} +1585 -1405
  15. package/dist/chunk-4NOQNTFI.js.map +1 -0
  16. package/dist/{chunk-I65SSQ5Z.mjs → chunk-6UMB5LTQ.mjs} +157 -7
  17. package/dist/chunk-6UMB5LTQ.mjs.map +1 -0
  18. package/dist/{chunk-FDJQRIMY.js → chunk-N4YZ45SK.js} +174 -24
  19. package/dist/chunk-N4YZ45SK.js.map +1 -0
  20. package/dist/{chunk-NB6TIKHK.mjs → chunk-NQV5RDCK.mjs} +2524 -2344
  21. package/dist/chunk-NQV5RDCK.mjs.map +1 -0
  22. package/dist/{chunk-NZOUEN67.mjs → chunk-PV5V6CVW.mjs} +38 -29
  23. package/dist/{chunk-NZOUEN67.mjs.map → chunk-PV5V6CVW.mjs.map} +1 -1
  24. package/dist/{chunk-X4YDETTD.js → chunk-ZEJSPTHS.js} +39 -30
  25. package/dist/chunk-ZEJSPTHS.js.map +1 -0
  26. package/dist/client/index.d.mts +6 -24
  27. package/dist/client/index.d.ts +6 -24
  28. package/dist/client/index.js +4 -10
  29. package/dist/client/index.js.map +1 -1
  30. package/dist/client/index.mjs +3 -9
  31. package/dist/components/index.d.mts +55 -39
  32. package/dist/components/index.d.ts +55 -39
  33. package/dist/components/index.js +4 -8
  34. package/dist/components/index.js.map +1 -1
  35. package/dist/components/index.mjs +5 -9
  36. package/dist/{config-B3jKt9P7.d.ts → config-B5oBQVEA.d.ts} +1 -1
  37. package/dist/{config-DkHF61xA.d.mts → config-Bx_uh22h.d.mts} +1 -1
  38. package/dist/contexts/index.d.mts +41 -4
  39. package/dist/contexts/index.d.ts +41 -4
  40. package/dist/contexts/index.js +8 -4
  41. package/dist/contexts/index.js.map +1 -1
  42. package/dist/contexts/index.mjs +7 -3
  43. package/dist/core/index.d.mts +51 -11
  44. package/dist/core/index.d.ts +51 -11
  45. package/dist/core/index.js +8 -2
  46. package/dist/core/index.js.map +1 -1
  47. package/dist/core/index.mjs +7 -1
  48. package/dist/index.d.mts +117 -20
  49. package/dist/index.d.ts +117 -20
  50. package/dist/index.js +11 -3
  51. package/dist/index.js.map +1 -1
  52. package/dist/index.mjs +10 -2
  53. package/dist/{notification.interface-DG6obXUH.d.mts → notification.interface-DLZGtV7Z.d.mts} +1 -1
  54. package/dist/{notification.interface-DcSuc9CL.d.ts → notification.interface-aLEJbA_g.d.ts} +1 -1
  55. package/dist/{s3.service-DGilbikH.d.mts → s3.service-CVgLWaDc.d.mts} +2 -2
  56. package/dist/{s3.service-DjwEQJPe.d.ts → s3.service-SLlX0Zbz.d.ts} +2 -2
  57. package/dist/server/index.d.mts +3 -3
  58. package/dist/server/index.d.ts +3 -3
  59. package/dist/server/index.js +3 -3
  60. package/dist/server/index.mjs +1 -1
  61. package/dist/useDataListRetriever-BqJSFBck.d.mts +33 -0
  62. package/dist/useDataListRetriever-BqJSFBck.d.ts +33 -0
  63. package/dist/{useSocket-CmzVtg32.d.mts → useSocket-BkxHHujj.d.mts} +1 -1
  64. package/dist/{useSocket-8eUtnL7J.d.ts → useSocket-CMDjWFYm.d.ts} +1 -1
  65. package/package.json +1 -1
  66. package/src/client/index.ts +0 -4
  67. package/src/components/index.ts +0 -3
  68. package/src/contexts/index.ts +1 -0
  69. package/src/core/index.ts +2 -0
  70. package/src/core/registry/ModuleRegistry.ts +2 -0
  71. package/src/features/assistant/components/parts/AssistantThread.tsx +1 -1
  72. package/src/features/assistant-message/AssistantMessageModule.ts +4 -0
  73. package/src/features/assistant-message/components/MessageItem.tsx +7 -7
  74. package/src/features/assistant-message/components/MessageList.tsx +1 -1
  75. package/src/features/assistant-message/components/__tests__/MessageItem.spec.tsx +11 -7
  76. package/src/features/assistant-message/components/index.ts +1 -0
  77. package/src/features/assistant-message/components/parts/MessageSourcesContainer.tsx +135 -0
  78. package/src/features/assistant-message/components/parts/MessageSourcesPanel.tsx +151 -0
  79. package/src/features/assistant-message/components/parts/RelevanceMeter.tsx +29 -0
  80. package/src/features/assistant-message/components/parts/__tests__/MessageSourcesPanel.spec.tsx +70 -0
  81. package/src/features/assistant-message/components/parts/tabs/CitationsTab.tsx +105 -0
  82. package/src/features/assistant-message/components/parts/tabs/ContentsTab.tsx +88 -0
  83. package/src/features/assistant-message/components/parts/tabs/ReferencesTab.tsx +51 -0
  84. package/src/features/assistant-message/components/parts/tabs/SuggestedQuestionsTab.tsx +24 -0
  85. package/src/features/assistant-message/components/parts/tabs/UsersTab.tsx +142 -0
  86. package/src/features/assistant-message/data/AssistantMessage.ts +20 -0
  87. package/src/features/assistant-message/data/AssistantMessageInterface.ts +2 -0
  88. package/src/features/assistant-message/data/AssistantMessageService.ts +13 -4
  89. package/src/features/assistant-message/data/__tests__/AssistantMessage.citations.spec.ts +65 -0
  90. package/src/features/assistant-message/data/__tests__/AssistantMessage.spec.ts +8 -0
  91. package/src/features/chunk/ChunkModule.ts +18 -0
  92. package/src/features/chunk/data/Chunk.ts +49 -0
  93. package/src/features/chunk/data/ChunkInput.ts +3 -0
  94. package/src/features/chunk/data/ChunkInterface.ts +18 -0
  95. package/src/features/chunk/data/__tests__/Chunk.spec.ts +83 -0
  96. package/src/features/chunk/data/index.ts +3 -0
  97. package/src/features/chunk/index.ts +2 -0
  98. package/src/features/rbac/components/RbacContainer.tsx +318 -49
  99. package/src/features/rbac/components/RbacPermissionPicker.tsx +144 -121
  100. package/src/features/rbac/contexts/RbacContext.tsx +209 -0
  101. package/src/features/rbac/contexts/index.ts +1 -0
  102. package/src/features/rbac/data/RbacMatrixModel.ts +84 -0
  103. package/src/features/rbac/data/RbacService.ts +61 -33
  104. package/src/features/rbac/data/RbacTypes.ts +28 -0
  105. package/src/features/rbac/data/index.ts +1 -0
  106. package/src/features/rbac/index.ts +1 -10
  107. package/src/features/rbac/rbac.module.ts +13 -0
  108. package/dist/ModulePathsInterface-BrdqgteS.d.mts +0 -31
  109. package/dist/ModulePathsInterface-DJKs7s_s.d.ts +0 -31
  110. package/dist/chunk-FDJQRIMY.js.map +0 -1
  111. package/dist/chunk-I65SSQ5Z.mjs.map +0 -1
  112. package/dist/chunk-NB6TIKHK.mjs.map +0 -1
  113. package/dist/chunk-X4YDETTD.js.map +0 -1
  114. package/dist/chunk-ZEDB6JVB.js.map +0 -1
  115. package/dist/useRbacState-C88O-5L8.d.ts +0 -77
  116. package/dist/useRbacState-mqYiRp3J.d.mts +0 -77
  117. package/src/features/assistant-message/components/parts/ReferenceBadges.tsx +0 -46
  118. package/src/features/assistant-message/components/parts/SuggestedFollowUps.tsx +0 -52
  119. package/src/features/assistant-message/components/parts/__tests__/ReferenceBadges.spec.tsx +0 -59
  120. package/src/features/assistant-message/components/parts/__tests__/SuggestedFollowUps.spec.tsx +0 -29
  121. package/src/features/rbac/components/RbacFeatureSection.tsx +0 -66
  122. package/src/features/rbac/components/RbacModuleTable.tsx +0 -121
  123. package/src/features/rbac/components/RbacToolbar.tsx +0 -40
  124. package/src/features/rbac/hooks/useRbacState.test.ts +0 -180
  125. package/src/features/rbac/hooks/useRbacState.ts +0 -319
  126. package/src/features/rbac/utils/RbacMigrationGenerator.test.ts +0 -124
  127. package/src/features/rbac/utils/RbacMigrationGenerator.ts +0 -184
  128. /package/dist/{BlockNoteEditor-JXK3JGKJ.mjs.map → BlockNoteEditor-3M5PD3BZ.mjs.map} +0 -0
@@ -15,7 +15,7 @@ var _chunk3ZPK4QOBjs = require('../chunk-3ZPK4QOB.js');
15
15
 
16
16
 
17
17
 
18
- var _chunkFDJQRIMYjs = require('../chunk-FDJQRIMY.js');
18
+ var _chunkN4YZ45SKjs = require('../chunk-N4YZ45SK.js');
19
19
  require('../chunk-LXKSUWAV.js');
20
20
  require('../chunk-IBS6NI7D.js');
21
21
 
@@ -86,7 +86,7 @@ var ServerSession = class {
86
86
  if (!rawModules) return false;
87
87
  const modules = JSON.parse(_pako2.default.ungzip(Buffer.from(rawModules, "base64"), { to: "string" }));
88
88
  const selectedModule = modules.find((module) => module.id === params.module.moduleId);
89
- return _chunkFDJQRIMYjs.checkPermissionsFromServer.call(void 0, {
89
+ return _chunkN4YZ45SKjs.checkPermissionsFromServer.call(void 0, {
90
90
  module: params.module,
91
91
  action: params.action,
92
92
  data: params.data,
@@ -296,5 +296,5 @@ _chunk7QVYU63Ejs.__name.call(void 0, ServerJsonApiDelete, "ServerJsonApiDelete")
296
296
 
297
297
 
298
298
 
299
- exports.ServerAuthService = _chunkFDJQRIMYjs.AuthService; exports.ServerCompanyService = _chunkFDJQRIMYjs.CompanyService; exports.ServerContentService = _chunkFDJQRIMYjs.ContentService; exports.ServerFeatureService = _chunkFDJQRIMYjs.FeatureService; exports.ServerJsonApiDelete = ServerJsonApiDelete; exports.ServerJsonApiGet = ServerJsonApiGet; exports.ServerJsonApiPatch = ServerJsonApiPatch; exports.ServerJsonApiPost = ServerJsonApiPost; exports.ServerJsonApiPut = ServerJsonApiPut; exports.ServerNotificationService = _chunkFDJQRIMYjs.NotificationService; exports.ServerPushService = _chunkFDJQRIMYjs.PushService; exports.ServerRoleService = _chunkFDJQRIMYjs.RoleService; exports.ServerS3Service = _chunkFDJQRIMYjs.S3Service; exports.ServerSession = ServerSession; exports.ServerUserService = _chunkFDJQRIMYjs.UserService; exports.configureServerJsonApi = configureServerJsonApi; exports.getServerApiUrl = getServerApiUrl; exports.getServerAppUrl = getServerAppUrl; exports.getServerToken = _chunkYUO55Q5Ajs.getServerToken; exports.getServerTrackablePages = getServerTrackablePages; exports.invalidateCacheTag = invalidateCacheTag; exports.invalidateCacheTags = invalidateCacheTags; exports.serverRequest = _chunk3ZPK4QOBjs.serverRequest;
299
+ exports.ServerAuthService = _chunkN4YZ45SKjs.AuthService; exports.ServerCompanyService = _chunkN4YZ45SKjs.CompanyService; exports.ServerContentService = _chunkN4YZ45SKjs.ContentService; exports.ServerFeatureService = _chunkN4YZ45SKjs.FeatureService; exports.ServerJsonApiDelete = ServerJsonApiDelete; exports.ServerJsonApiGet = ServerJsonApiGet; exports.ServerJsonApiPatch = ServerJsonApiPatch; exports.ServerJsonApiPost = ServerJsonApiPost; exports.ServerJsonApiPut = ServerJsonApiPut; exports.ServerNotificationService = _chunkN4YZ45SKjs.NotificationService; exports.ServerPushService = _chunkN4YZ45SKjs.PushService; exports.ServerRoleService = _chunkN4YZ45SKjs.RoleService; exports.ServerS3Service = _chunkN4YZ45SKjs.S3Service; exports.ServerSession = ServerSession; exports.ServerUserService = _chunkN4YZ45SKjs.UserService; exports.configureServerJsonApi = configureServerJsonApi; exports.getServerApiUrl = getServerApiUrl; exports.getServerAppUrl = getServerAppUrl; exports.getServerToken = _chunkYUO55Q5Ajs.getServerToken; exports.getServerTrackablePages = getServerTrackablePages; exports.invalidateCacheTag = invalidateCacheTag; exports.invalidateCacheTags = invalidateCacheTags; exports.serverRequest = _chunk3ZPK4QOBjs.serverRequest;
300
300
  //# sourceMappingURL=index.js.map
@@ -15,7 +15,7 @@ import {
15
15
  S3Service,
16
16
  UserService,
17
17
  checkPermissionsFromServer
18
- } from "../chunk-I65SSQ5Z.mjs";
18
+ } from "../chunk-6UMB5LTQ.mjs";
19
19
  import "../chunk-AUXK7QSA.mjs";
20
20
  import "../chunk-C7C7VY4F.mjs";
21
21
  import {
@@ -0,0 +1,33 @@
1
+ type PageInfo = {
2
+ startItem: number;
3
+ endItem: number;
4
+ pageSize: number;
5
+ };
6
+ type DataListRetriever<T> = {
7
+ ready?: boolean;
8
+ setReady: (state: boolean) => void;
9
+ isLoaded: boolean;
10
+ data: T[] | undefined;
11
+ total?: number;
12
+ next?: (onlyNewRecords?: boolean) => Promise<void>;
13
+ previous?: (onlyNewRecords?: boolean) => Promise<void>;
14
+ search: (search: string) => Promise<void>;
15
+ refresh: () => Promise<void>;
16
+ addAdditionalParameter: (key: string, value: any | null) => void;
17
+ removeAdditionalParameter: (key: string) => void;
18
+ setRefreshedElement: (element: T) => void;
19
+ removeElement: (element: T) => void;
20
+ isSearch: boolean;
21
+ pageInfo?: PageInfo;
22
+ };
23
+ declare function useDataListRetriever<T>(params: {
24
+ ready?: boolean;
25
+ retriever: (params: any) => Promise<T[]>;
26
+ retrieverParams?: any;
27
+ search?: string;
28
+ addAdditionalParameter?: (key: string, value: any | null) => void;
29
+ requiresSearch?: boolean;
30
+ module: any;
31
+ }): DataListRetriever<T>;
32
+
33
+ export { type DataListRetriever as D, useDataListRetriever as u };
@@ -0,0 +1,33 @@
1
+ type PageInfo = {
2
+ startItem: number;
3
+ endItem: number;
4
+ pageSize: number;
5
+ };
6
+ type DataListRetriever<T> = {
7
+ ready?: boolean;
8
+ setReady: (state: boolean) => void;
9
+ isLoaded: boolean;
10
+ data: T[] | undefined;
11
+ total?: number;
12
+ next?: (onlyNewRecords?: boolean) => Promise<void>;
13
+ previous?: (onlyNewRecords?: boolean) => Promise<void>;
14
+ search: (search: string) => Promise<void>;
15
+ refresh: () => Promise<void>;
16
+ addAdditionalParameter: (key: string, value: any | null) => void;
17
+ removeAdditionalParameter: (key: string) => void;
18
+ setRefreshedElement: (element: T) => void;
19
+ removeElement: (element: T) => void;
20
+ isSearch: boolean;
21
+ pageInfo?: PageInfo;
22
+ };
23
+ declare function useDataListRetriever<T>(params: {
24
+ ready?: boolean;
25
+ retriever: (params: any) => Promise<T[]>;
26
+ retrieverParams?: any;
27
+ search?: string;
28
+ addAdditionalParameter?: (key: string, value: any | null) => void;
29
+ requiresSearch?: boolean;
30
+ module: any;
31
+ }): DataListRetriever<T>;
32
+
33
+ export { type DataListRetriever as D, useDataListRetriever as u };
@@ -1,4 +1,4 @@
1
- import { N as NotificationInterface } from './notification.interface-DG6obXUH.mjs';
1
+ import { N as NotificationInterface } from './notification.interface-DLZGtV7Z.mjs';
2
2
 
3
3
  interface UseSocketOptions {
4
4
  token: string;
@@ -1,4 +1,4 @@
1
- import { N as NotificationInterface } from './notification.interface-DcSuc9CL.js';
1
+ import { N as NotificationInterface } from './notification.interface-aLEJbA_g.js';
2
2
 
3
3
  interface UseSocketOptions {
4
4
  token: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@carlonicora/nextjs-jsonapi",
3
- "version": "1.78.0",
3
+ "version": "1.80.0",
4
4
  "description": "Next.js JSON:API client with server/client support and caching",
5
5
  "author": "Carlo Nicora",
6
6
  "license": "GPL-3.0-or-later",
@@ -27,10 +27,6 @@ export * from "../features/user/hooks";
27
27
  export * from "../features/oauth/hooks";
28
28
  export * from "../features/company/hooks/useSubscriptionStatus";
29
29
 
30
- // RBAC hooks and utils
31
- export { useRbacState } from "../features/rbac/hooks/useRbacState";
32
- export { generateMigrationFile, downloadMigrationFile } from "../features/rbac/utils/RbacMigrationGenerator";
33
-
34
30
  registerTableGenerator("roles", useRoleTableStructure);
35
31
  registerTableGenerator("users", useUserTableStructure);
36
32
  registerTableGenerator("companies", useCompanyTableStructure);
@@ -31,9 +31,6 @@ export * from "../features/user/components";
31
31
  export * from "../features/oauth/components";
32
32
  export * from "../features/waitlist/components";
33
33
  export { RbacContainer } from "../features/rbac/components/RbacContainer";
34
- export { RbacToolbar } from "../features/rbac/components/RbacToolbar";
35
- export { RbacFeatureSection } from "../features/rbac/components/RbacFeatureSection";
36
- export { RbacModuleTable } from "../features/rbac/components/RbacModuleTable";
37
34
  export { RbacPermissionCell } from "../features/rbac/components/RbacPermissionCell";
38
35
  export { RbacPermissionPicker } from "../features/rbac/components/RbacPermissionPicker";
39
36
 
@@ -4,6 +4,7 @@ export * from "../features/onboarding/contexts";
4
4
  export * from "../features/role/contexts/RoleContext";
5
5
  export * from "../features/user/contexts";
6
6
  export * from "../features/how-to/contexts/HowToContext";
7
+ export * from "../features/rbac/contexts/RbacContext";
7
8
  export * from "../features/assistant/contexts/AssistantContext";
8
9
  export * from "./CommonContext";
9
10
  export * from "./HeaderChildrenContext";
package/src/core/index.ts CHANGED
@@ -65,6 +65,8 @@ export * from "../features/assistant/AssistantModule";
65
65
  export * from "../features/assistant/data";
66
66
  export * from "../features/assistant-message/AssistantMessageModule";
67
67
  export * from "../features/assistant-message/data";
68
+ export * from "../features/chunk/ChunkModule";
69
+ export * from "../features/chunk/data";
68
70
  export * from "../features/feature/data";
69
71
  export * from "../features/feature/feature.module";
70
72
  export * from "../features/module";
@@ -18,6 +18,7 @@ export interface FoundationModuleDefinitions {
18
18
  HowTo: ModuleWithPermissions;
19
19
  Assistant: ModuleWithPermissions;
20
20
  AssistantMessage: ModuleWithPermissions;
21
+ Chunk: ModuleWithPermissions;
21
22
  // Billing modules - READ: all users, UPDATE: CompanyAdministrator, ADMIN: Administrator
22
23
  Billing: ModuleWithPermissions;
23
24
  StripeCustomer: ModuleWithPermissions;
@@ -54,6 +55,7 @@ export interface FoundationModuleDefinitions {
54
55
  // RBAC modules
55
56
  PermissionMapping: ModuleWithPermissions;
56
57
  ModulePaths: ModuleWithPermissions;
58
+ RbacMatrix: ModuleWithPermissions;
57
59
  // Audit modules
58
60
  AuditLog: ModuleWithPermissions;
59
61
  }
@@ -22,7 +22,7 @@ export function AssistantThread({ messages, sending, status, onSelectFollowUp, f
22
22
  }, [messages.length, sending]);
23
23
 
24
24
  return (
25
- <div className="flex-1 overflow-y-auto px-6 py-5">
25
+ <div className="flex-1 min-w-0 overflow-x-hidden overflow-y-auto px-6 py-5">
26
26
  <MessageList
27
27
  messages={messages}
28
28
  onSelectFollowUp={onSelectFollowUp}
@@ -12,6 +12,7 @@ export const AssistantMessageModule = (factory: ModuleFactory) =>
12
12
  icon: MessageSquareIcon,
13
13
  inclusions: {
14
14
  lists: {
15
+ types: [`references`, `citations`, `citations.source`, `citations.source.author`],
15
16
  fields: [
16
17
  createJsonApiInclusion("assistant-messages", [
17
18
  `role`,
@@ -21,7 +22,10 @@ export const AssistantMessageModule = (factory: ModuleFactory) =>
21
22
  `inputTokens`,
22
23
  `outputTokens`,
23
24
  `references`,
25
+ `citations`,
24
26
  ]),
27
+ createJsonApiInclusion("chunks", [`content`, `nodeId`, `nodeType`, `imagePath`, `source`]),
28
+ createJsonApiInclusion("users", [`name`, `avatar`]),
25
29
  ],
26
30
  },
27
31
  },
@@ -5,8 +5,7 @@ import { Sparkles, AlertCircle } from "lucide-react";
5
5
  import ReactMarkdown from "react-markdown";
6
6
  import remarkGfm from "remark-gfm";
7
7
  import type { AssistantMessageInterface } from "../data/AssistantMessageInterface";
8
- import { ReferenceBadges } from "./parts/ReferenceBadges";
9
- import { SuggestedFollowUps } from "./parts/SuggestedFollowUps";
8
+ import { MessageSourcesContainer } from "./parts/MessageSourcesContainer";
10
9
 
11
10
  interface Props {
12
11
  message: AssistantMessageInterface;
@@ -41,7 +40,7 @@ export function MessageItem({ message, isLatestAssistant, onSelectFollowUp, fail
41
40
  }
42
41
 
43
42
  return (
44
- <div className="flex max-w-[78%] flex-col gap-1.5">
43
+ <div className="flex min-w-0 max-w-[78%] flex-col gap-1.5">
45
44
  <div className="text-muted-foreground flex items-center gap-2 pl-1 text-xs">
46
45
  <span className="flex h-4 w-4 items-center justify-center rounded-full bg-gradient-to-br from-blue-400 to-violet-500 text-white">
47
46
  <Sparkles className="h-2.5 w-2.5" />
@@ -51,10 +50,11 @@ export function MessageItem({ message, isLatestAssistant, onSelectFollowUp, fail
51
50
  <div className="bg-muted text-foreground rounded-2xl rounded-bl-sm px-3.5 py-2.5 text-sm leading-relaxed">
52
51
  <ReactMarkdown remarkPlugins={[remarkGfm]}>{message.content}</ReactMarkdown>
53
52
  </div>
54
- <ReferenceBadges references={message.references} />
55
- {isLatestAssistant && (
56
- <SuggestedFollowUps questions={message.suggestedQuestions ?? []} onSelect={onSelectFollowUp} />
57
- )}
53
+ <MessageSourcesContainer
54
+ message={message}
55
+ isLatestAssistant={isLatestAssistant}
56
+ onSelectFollowUp={onSelectFollowUp}
57
+ />
58
58
  </div>
59
59
  );
60
60
  }
@@ -22,7 +22,7 @@ export function MessageList({ messages, onSelectFollowUp, failedMessageIds, onRe
22
22
  }
23
23
 
24
24
  return (
25
- <div className="flex flex-col gap-y-3">
25
+ <div className="flex min-w-0 flex-col gap-y-3">
26
26
  {ordered.map((m, i) => (
27
27
  <MessageItem
28
28
  key={m.id}
@@ -8,7 +8,7 @@ import type { ApiDataInterface } from "../../../../core";
8
8
  import type { AssistantMessageInterface } from "../../data/AssistantMessageInterface";
9
9
  import { MessageItem } from "../MessageItem";
10
10
 
11
- // Test-only account model to exercise ReferenceBadges
11
+ // Test-only account model to exercise the sources panel references list
12
12
  class TestAccount extends AbstractApiData {
13
13
  static identifierFields: string[] = ["name"];
14
14
  rehydrate(data: any): this {
@@ -51,6 +51,7 @@ function buildMessageStub(p: {
51
51
  content: p.content ?? "",
52
52
  position: 0,
53
53
  references: p.references ?? [],
54
+ citations: [],
54
55
  suggestedQuestions: p.suggestedQuestions ?? [],
55
56
  createdAt: new Date(),
56
57
  updatedAt: new Date(),
@@ -66,7 +67,7 @@ describe("MessageItem", () => {
66
67
  expect(screen.queryByText("features.assistant.agent_name")).not.toBeInTheDocument();
67
68
  });
68
69
 
69
- it("assistant message renders bubble, references, and suggestions toggle when latest", () => {
70
+ it("assistant message renders agent label and the sources panel toggle when sources exist", () => {
70
71
  const msg = buildMessageStub({
71
72
  role: "assistant",
72
73
  content: "**bold** reply",
@@ -75,14 +76,17 @@ describe("MessageItem", () => {
75
76
  });
76
77
  render(<MessageItem message={msg} isLatestAssistant={true} onSelectFollowUp={vi.fn()} />);
77
78
  expect(screen.getByText("features.assistant.agent_name")).toBeInTheDocument();
78
- expect(screen.getByRole("link", { name: /acme/i })).toBeInTheDocument();
79
- expect(screen.getByRole("button", { name: /show_suggestions/ })).toBeInTheDocument();
79
+ // The MessageSourcesPanel toggle uses the message.sources.toggle translation key.
80
+ expect(screen.getByRole("button", { name: /sources\.toggle/i })).toBeInTheDocument();
80
81
  });
81
82
 
82
- it("assistant message NOT latest: no suggestions toggle", () => {
83
+ it("assistant message NOT latest with only suggestions: no sources panel rendered", () => {
83
84
  const msg = buildMessageStub({ role: "assistant", content: "prior", suggestedQuestions: ["x"] });
84
- render(<MessageItem message={msg} isLatestAssistant={false} onSelectFollowUp={vi.fn()} />);
85
- expect(screen.queryByRole("button", { name: /show_suggestions/ })).not.toBeInTheDocument();
85
+ const { container } = render(<MessageItem message={msg} isLatestAssistant={false} onSelectFollowUp={vi.fn()} />);
86
+ // No sources/citations/references and suggestions are gated by isLatestAssistant -> panel renders nothing.
87
+ expect(screen.queryByRole("button", { name: /sources\.toggle/i })).not.toBeInTheDocument();
88
+ // Sanity: the assistant bubble itself still rendered.
89
+ expect(container).not.toBeEmptyDOMElement();
86
90
  });
87
91
 
88
92
  it("failed user message: renders retry control and calls onRetry with the id", async () => {
@@ -1,2 +1,3 @@
1
1
  export * from "./MessageItem";
2
2
  export * from "./MessageList";
3
+ export { MessageSourcesPanel } from "./parts/MessageSourcesPanel";
@@ -0,0 +1,135 @@
1
+ "use client";
2
+
3
+ import { useEffect, useMemo, useState } from "react";
4
+ import { ClientAbstractService, ClientHttpMethod, EndpointCreator, ModuleRegistry } from "../../../../core";
5
+ import type { ApiDataInterface, ApiRequestDataTypeInterface } from "../../../../core";
6
+ import type { AssistantMessageInterface } from "../../data/AssistantMessageInterface";
7
+ import { MessageSourcesPanel } from "./MessageSourcesPanel";
8
+
9
+ interface Props {
10
+ message: AssistantMessageInterface;
11
+ isLatestAssistant: boolean;
12
+ onSelectFollowUp: (q: string) => void;
13
+ }
14
+
15
+ class SourcesFetcher extends ClientAbstractService {
16
+ static async findManyByIds(params: {
17
+ module: ApiRequestDataTypeInterface;
18
+ idsParam: string;
19
+ ids: string[];
20
+ }): Promise<ApiDataInterface[]> {
21
+ const endpoint = new EndpointCreator({ endpoint: params.module });
22
+ endpoint.addAdditionalParam(params.idsParam, params.ids.join(","));
23
+ if (params.module.inclusions?.lists?.fields) endpoint.limitToFields(params.module.inclusions.lists.fields);
24
+ if (params.module.inclusions?.lists?.types) endpoint.limitToType(params.module.inclusions.lists.types);
25
+ return this.callApi<ApiDataInterface[]>({
26
+ type: params.module,
27
+ method: ClientHttpMethod.GET,
28
+ endpoint: endpoint.generate(),
29
+ });
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Library-level data wrapper around `MessageSourcesPanel`. Reads
35
+ * `chunk.nodeId` / `chunk.nodeType` from the message's citations, dynamically
36
+ * resolves the matching module via `ModuleRegistry.findByModelName(nodeType)`,
37
+ * fetches the entities in one request per type, and supplies them (plus their
38
+ * deduplicated `author` users) to the presentational panel.
39
+ */
40
+ export function MessageSourcesContainer({ message, isLatestAssistant, onSelectFollowUp }: Props) {
41
+ // Group nodeIds per nodeType so we issue one request per source type.
42
+ const groups = useMemo(() => {
43
+ const map = new Map<string, Set<string>>();
44
+ for (const chunk of message.citations) {
45
+ if (!chunk.nodeType || !chunk.nodeId) continue;
46
+ let bucket = map.get(chunk.nodeType);
47
+ if (!bucket) {
48
+ bucket = new Set<string>();
49
+ map.set(chunk.nodeType, bucket);
50
+ }
51
+ bucket.add(chunk.nodeId);
52
+ }
53
+ return map;
54
+ }, [message.citations]);
55
+
56
+ // Flatten to a deterministic key so useEffect re-runs when ids change.
57
+ const groupsKey = useMemo(() => {
58
+ const parts: string[] = [];
59
+ for (const [nodeType, ids] of groups) {
60
+ parts.push(`${nodeType}:${Array.from(ids).sort().join(",")}`);
61
+ }
62
+ return parts.sort().join("|");
63
+ }, [groups]);
64
+
65
+ const [resolved, setResolved] = useState<ApiDataInterface[]>([]);
66
+
67
+ useEffect(() => {
68
+ if (groups.size === 0) {
69
+ setResolved([]);
70
+ return;
71
+ }
72
+ let cancelled = false;
73
+ const requests: Promise<ApiDataInterface[]>[] = [];
74
+ for (const [nodeType, idSet] of groups) {
75
+ let module: ApiRequestDataTypeInterface | undefined;
76
+ try {
77
+ module = ModuleRegistry.findByModelName(nodeType);
78
+ } catch {
79
+ continue; // No registered module for this nodeType — skip silently.
80
+ }
81
+ const idsParam = `${(module as any).nodeName ?? module.name.replace(/s$/, "")}Ids`;
82
+ requests.push(
83
+ SourcesFetcher.findManyByIds({
84
+ module,
85
+ idsParam,
86
+ ids: Array.from(idSet),
87
+ }).catch((error) => {
88
+ console.error(`Failed to load sources for ${nodeType}:`, error);
89
+ return [] as ApiDataInterface[];
90
+ }),
91
+ );
92
+ }
93
+ Promise.all(requests).then((results) => {
94
+ if (cancelled) return;
95
+ setResolved(results.flat());
96
+ });
97
+ return () => {
98
+ cancelled = true;
99
+ };
100
+ }, [groupsKey, groups]);
101
+
102
+ const sources = useMemo(() => {
103
+ const map = new Map<string, ApiDataInterface>();
104
+ for (const entity of resolved) map.set(entity.id, entity);
105
+ return map;
106
+ }, [resolved]);
107
+
108
+ const users = useMemo(() => {
109
+ const userMap = new Map<string, ApiDataInterface>();
110
+ for (const entity of resolved) {
111
+ // Content.author getter throws when missing; tolerate either shape.
112
+ const author =
113
+ (entity as any)._author ??
114
+ (() => {
115
+ try {
116
+ return (entity as any).author;
117
+ } catch {
118
+ return undefined;
119
+ }
120
+ })();
121
+ if (author?.id) userMap.set(author.id, author);
122
+ }
123
+ return Array.from(userMap.values());
124
+ }, [resolved]);
125
+
126
+ return (
127
+ <MessageSourcesPanel
128
+ message={message}
129
+ isLatestAssistant={isLatestAssistant}
130
+ onSelectFollowUp={onSelectFollowUp}
131
+ sources={sources}
132
+ users={users}
133
+ />
134
+ );
135
+ }
@@ -0,0 +1,151 @@
1
+ "use client";
2
+
3
+ import { useMemo, useState } from "react";
4
+ import { useTranslations } from "next-intl";
5
+ import { ChevronDown, ChevronRight } from "lucide-react";
6
+ import type { ApiDataInterface } from "../../../../core";
7
+ import { ModuleRegistry } from "../../../../core/registry/ModuleRegistry";
8
+ import type { AssistantMessageInterface } from "../../data/AssistantMessageInterface";
9
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../../../shadcnui/ui/tabs";
10
+ import { ReferencesTab } from "./tabs/ReferencesTab";
11
+ import { CitationsTab } from "./tabs/CitationsTab";
12
+ import { ContentsTab } from "./tabs/ContentsTab";
13
+ import { UsersTab } from "./tabs/UsersTab";
14
+ import { SuggestedQuestionsTab } from "./tabs/SuggestedQuestionsTab";
15
+
16
+ interface Props {
17
+ message: AssistantMessageInterface;
18
+ isLatestAssistant: boolean;
19
+ onSelectFollowUp: (q: string) => void;
20
+ /**
21
+ * Map of resolved source entities keyed by `chunk.nodeId`. When provided, the
22
+ * Contents and Citations tabs use these to render real entity names. The
23
+ * library is presentational; the app fetches and supplies these.
24
+ */
25
+ sources?: Map<string, ApiDataInterface>;
26
+ /**
27
+ * Deduplicated list of authors derived from the resolved sources. When
28
+ * provided, the Users tab is populated from this list.
29
+ */
30
+ users?: ApiDataInterface[];
31
+ }
32
+
33
+ type TabKey = "suggested" | "references" | "citations" | "contents" | "users";
34
+
35
+ export function MessageSourcesPanel({ message, isLatestAssistant, onSelectFollowUp, sources, users }: Props) {
36
+ const t = useTranslations();
37
+
38
+ // Filter out references whose module has no pageUrl: those are
39
+ // intermediary/relational nodes (e.g., BomItem, BomTreeNode) that the user
40
+ // shouldn't navigate to directly.
41
+ const visibleReferences = useMemo(
42
+ () =>
43
+ message.references.filter((ref) => {
44
+ try {
45
+ return !!ModuleRegistry.findByName(ref.type).pageUrl;
46
+ } catch {
47
+ return false;
48
+ }
49
+ }),
50
+ [message.references],
51
+ );
52
+
53
+ const refsCount = visibleReferences.length;
54
+ const citationsCount = message.citations.length;
55
+ const suggestionsCount = isLatestAssistant ? message.suggestedQuestions.length : 0;
56
+
57
+ const contentsCount = useMemo(() => {
58
+ if (sources) return sources.size;
59
+ // Fallback: derive from unique nodeId on chunks when sources haven't been
60
+ // supplied yet (e.g., during the initial fetch).
61
+ const ids = new Set<string>();
62
+ for (const c of message.citations) {
63
+ if (c.nodeId) ids.add(c.nodeId);
64
+ }
65
+ return ids.size;
66
+ }, [message.citations, sources]);
67
+
68
+ const usersCount = users?.length ?? 0;
69
+
70
+ const total = refsCount + citationsCount + contentsCount + usersCount + suggestionsCount;
71
+
72
+ const visibleTabs: TabKey[] = [];
73
+ if (suggestionsCount > 0) visibleTabs.push("suggested");
74
+ if (refsCount > 0) visibleTabs.push("references");
75
+ if (citationsCount > 0) visibleTabs.push("citations");
76
+ if (contentsCount > 0) visibleTabs.push("contents");
77
+ if (usersCount > 0) visibleTabs.push("users");
78
+
79
+ const autoOpen = isLatestAssistant && suggestionsCount > 0;
80
+ const initialTab: TabKey | undefined = autoOpen ? "suggested" : visibleTabs[0];
81
+ const [open, setOpen] = useState(autoOpen);
82
+ const [active, setActive] = useState<TabKey | undefined>(initialTab);
83
+
84
+ if (total === 0) return null;
85
+
86
+ const panelId = `sources-panel-${message.id}`;
87
+
88
+ return (
89
+ <div className="mt-2 w-full min-w-0">
90
+ <button
91
+ type="button"
92
+ onClick={() => setOpen((v) => !v)}
93
+ aria-expanded={open}
94
+ aria-controls={panelId}
95
+ className="text-primary inline-flex items-center gap-1 text-xs font-medium"
96
+ >
97
+ {open ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
98
+ {open
99
+ ? t("features.assistant.message.sources.toggle_hide")
100
+ : t("features.assistant.message.sources.toggle", { count: total })}
101
+ </button>
102
+
103
+ {open && (
104
+ <div id={panelId} className="mt-2 w-full min-w-0">
105
+ <Tabs value={active} onValueChange={(v) => setActive(v as TabKey)}>
106
+ <TabsList>
107
+ {visibleTabs.map((key) => (
108
+ <TabsTrigger key={key} value={key}>
109
+ {t(`features.assistant.message.sources.tabs.${key === "suggested" ? "suggested_questions" : key}`)}
110
+ <span className="text-muted-foreground ml-1.5 text-[10px]">
111
+ {key === "suggested" && suggestionsCount}
112
+ {key === "references" && refsCount}
113
+ {key === "citations" && citationsCount}
114
+ {key === "contents" && contentsCount}
115
+ {key === "users" && usersCount}
116
+ </span>
117
+ </TabsTrigger>
118
+ ))}
119
+ </TabsList>
120
+
121
+ {suggestionsCount > 0 && (
122
+ <TabsContent value="suggested">
123
+ <SuggestedQuestionsTab questions={message.suggestedQuestions} onSelect={onSelectFollowUp} />
124
+ </TabsContent>
125
+ )}
126
+ {refsCount > 0 && (
127
+ <TabsContent value="references">
128
+ <ReferencesTab references={visibleReferences} />
129
+ </TabsContent>
130
+ )}
131
+ {citationsCount > 0 && (
132
+ <TabsContent value="citations">
133
+ <CitationsTab citations={message.citations} sources={sources} />
134
+ </TabsContent>
135
+ )}
136
+ {contentsCount > 0 && (
137
+ <TabsContent value="contents">
138
+ <ContentsTab citations={message.citations} sources={sources} />
139
+ </TabsContent>
140
+ )}
141
+ {usersCount > 0 && (
142
+ <TabsContent value="users">
143
+ <UsersTab users={users ?? []} citations={message.citations} sources={sources} />
144
+ </TabsContent>
145
+ )}
146
+ </Tabs>
147
+ </div>
148
+ )}
149
+ </div>
150
+ );
151
+ }
@@ -0,0 +1,29 @@
1
+ "use client";
2
+
3
+ interface Props {
4
+ /** 0-1 (decimal) or 0-100 (percent). Auto-detected. */
5
+ value: number;
6
+ className?: string;
7
+ }
8
+
9
+ export function RelevanceMeter({ value, className = "" }: Props) {
10
+ const pct = Math.max(0, Math.min(100, value <= 1 ? value * 100 : value));
11
+ const label = `${Math.round(pct)}%`;
12
+ return (
13
+ <div
14
+ role="meter"
15
+ aria-valuenow={Math.round(pct)}
16
+ aria-valuemin={0}
17
+ aria-valuemax={100}
18
+ aria-label={`Relevance ${label}`}
19
+ className={`bg-muted relative mx-auto flex h-5 w-20 items-center justify-center overflow-hidden rounded border ${className}`}
20
+ >
21
+ <div className="bg-accent absolute top-0 left-0 h-full" style={{ width: `${pct}%` }} />
22
+ <span
23
+ className={`relative text-xs ${pct < 40 ? "text-muted-foreground" : "text-accent-foreground font-semibold"}`}
24
+ >
25
+ {label}
26
+ </span>
27
+ </div>
28
+ );
29
+ }