@carlonicora/nextjs-jsonapi 1.77.3 → 1.79.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 (146) hide show
  1. package/dist/AssistantInterface-BYgI5z1-.d.mts +12 -0
  2. package/dist/AssistantInterface-DfDcz0gJ.d.ts +12 -0
  3. package/dist/AssistantMessageInterface-DWnbd6J7.d.ts +36 -0
  4. package/dist/AssistantMessageInterface-Mla6kgPe.d.mts +36 -0
  5. package/dist/{AuthComponent-Blbs06ud.d.ts → AuthComponent-B6DIk8Vf.d.ts} +1 -1
  6. package/dist/{AuthComponent-huIaK5rm.d.mts → AuthComponent-BKI0ZbtD.d.mts} +1 -1
  7. package/dist/{BlockNoteEditor-7HAAXN3H.mjs → BlockNoteEditor-6CBDTVKV.mjs} +4 -4
  8. package/dist/{BlockNoteEditor-UB7T7V67.js → BlockNoteEditor-EH4HWI7H.js} +14 -14
  9. package/dist/{BlockNoteEditor-UB7T7V67.js.map → BlockNoteEditor-EH4HWI7H.js.map} +1 -1
  10. package/dist/RbacTypes-BTbr27Ew.d.mts +43 -0
  11. package/dist/RbacTypes-BTbr27Ew.d.ts +43 -0
  12. package/dist/{auth.interface-CQJ6A2Cj.d.ts → auth.interface-BBUgMZzs.d.ts} +1 -1
  13. package/dist/{auth.interface-Bdq7-8iV.d.mts → auth.interface-XYEREOD6.d.mts} +1 -1
  14. package/dist/billing/index.js +346 -346
  15. package/dist/billing/index.mjs +3 -3
  16. package/dist/{chunk-FKLP4NED.js → chunk-5IEWLLLD.js} +379 -18
  17. package/dist/chunk-5IEWLLLD.js.map +1 -0
  18. package/dist/{chunk-XI35ALWY.mjs → chunk-BKM5U3DE.mjs} +362 -1
  19. package/dist/chunk-BKM5U3DE.mjs.map +1 -0
  20. package/dist/{chunk-F44ET4AC.mjs → chunk-ENRSFVOS.mjs} +2657 -2264
  21. package/dist/chunk-ENRSFVOS.mjs.map +1 -0
  22. package/dist/{chunk-JOJZRGZL.mjs → chunk-MEWXQEVE.mjs} +38 -29
  23. package/dist/{chunk-JOJZRGZL.mjs.map → chunk-MEWXQEVE.mjs.map} +1 -1
  24. package/dist/{chunk-OTZEXASK.js → chunk-TWDSDTHU.js} +39 -30
  25. package/dist/chunk-TWDSDTHU.js.map +1 -0
  26. package/dist/{chunk-CV7UOUKQ.js → chunk-ZDP3MBUI.js} +1813 -1420
  27. package/dist/chunk-ZDP3MBUI.js.map +1 -0
  28. package/dist/client/index.d.mts +6 -24
  29. package/dist/client/index.d.ts +6 -24
  30. package/dist/client/index.js +4 -10
  31. package/dist/client/index.js.map +1 -1
  32. package/dist/client/index.mjs +3 -9
  33. package/dist/components/index.d.mts +51 -34
  34. package/dist/components/index.d.ts +51 -34
  35. package/dist/components/index.js +4 -4
  36. package/dist/components/index.js.map +1 -1
  37. package/dist/components/index.mjs +9 -9
  38. package/dist/{config-B3jKt9P7.d.ts → config-B5oBQVEA.d.ts} +1 -1
  39. package/dist/{config-DkHF61xA.d.mts → config-Bx_uh22h.d.mts} +1 -1
  40. package/dist/contexts/index.d.mts +65 -4
  41. package/dist/contexts/index.d.ts +65 -4
  42. package/dist/contexts/index.js +12 -4
  43. package/dist/contexts/index.js.map +1 -1
  44. package/dist/contexts/index.mjs +11 -3
  45. package/dist/core/index.d.mts +126 -11
  46. package/dist/core/index.d.ts +126 -11
  47. package/dist/core/index.js +16 -2
  48. package/dist/core/index.js.map +1 -1
  49. package/dist/core/index.mjs +15 -1
  50. package/dist/index.d.mts +118 -20
  51. package/dist/index.d.ts +118 -20
  52. package/dist/index.js +19 -3
  53. package/dist/index.js.map +1 -1
  54. package/dist/index.mjs +18 -2
  55. package/dist/{notification.interface-DG6obXUH.d.mts → notification.interface-DLZGtV7Z.d.mts} +1 -1
  56. package/dist/{notification.interface-DcSuc9CL.d.ts → notification.interface-aLEJbA_g.d.ts} +1 -1
  57. package/dist/{s3.service-DGilbikH.d.mts → s3.service-CVgLWaDc.d.mts} +2 -2
  58. package/dist/{s3.service-DjwEQJPe.d.ts → s3.service-SLlX0Zbz.d.ts} +2 -2
  59. package/dist/server/index.d.mts +3 -3
  60. package/dist/server/index.d.ts +3 -3
  61. package/dist/server/index.js +3 -3
  62. package/dist/server/index.mjs +1 -1
  63. package/dist/useDataListRetriever-BqJSFBck.d.mts +33 -0
  64. package/dist/useDataListRetriever-BqJSFBck.d.ts +33 -0
  65. package/dist/{useSocket-CmzVtg32.d.mts → useSocket-BkxHHujj.d.mts} +1 -1
  66. package/dist/{useSocket-8eUtnL7J.d.ts → useSocket-CMDjWFYm.d.ts} +1 -1
  67. package/package.json +1 -1
  68. package/src/client/index.ts +0 -4
  69. package/src/components/index.ts +2 -3
  70. package/src/contexts/index.ts +2 -0
  71. package/src/core/index.ts +4 -0
  72. package/src/core/registry/ModuleRegistry.ts +10 -0
  73. package/src/features/assistant/AssistantModule.ts +19 -0
  74. package/src/features/assistant/components/containers/AssistantContainer.tsx +56 -0
  75. package/src/features/assistant/components/containers/__tests__/AssistantContainer.spec.tsx +101 -0
  76. package/src/features/assistant/components/index.ts +1 -0
  77. package/src/features/assistant/components/parts/AssistantComposer.tsx +56 -0
  78. package/src/features/assistant/components/parts/AssistantEmptyState.tsx +47 -0
  79. package/src/features/assistant/components/parts/AssistantSidebar.tsx +64 -0
  80. package/src/features/assistant/components/parts/AssistantStatusLine.tsx +19 -0
  81. package/src/features/assistant/components/parts/AssistantThread.tsx +36 -0
  82. package/src/features/assistant/components/parts/AssistantThreadHeader.tsx +91 -0
  83. package/src/features/assistant/components/parts/__tests__/AssistantComposer.spec.tsx +32 -0
  84. package/src/features/assistant/components/parts/__tests__/AssistantEmptyState.spec.tsx +27 -0
  85. package/src/features/assistant/components/parts/__tests__/AssistantSidebar.spec.tsx +58 -0
  86. package/src/features/assistant/components/parts/__tests__/AssistantStatusLine.spec.tsx +19 -0
  87. package/src/features/assistant/components/parts/__tests__/AssistantThread.spec.tsx +39 -0
  88. package/src/features/assistant/components/parts/__tests__/AssistantThreadHeader.spec.tsx +67 -0
  89. package/src/features/assistant/contexts/AssistantContext.tsx +255 -0
  90. package/src/features/assistant/contexts/__tests__/AssistantContext.spec.tsx +375 -0
  91. package/src/features/assistant/data/Assistant.ts +37 -0
  92. package/src/features/assistant/data/AssistantInterface.ts +11 -0
  93. package/src/features/assistant/data/AssistantService.ts +79 -0
  94. package/src/features/assistant/data/index.ts +3 -0
  95. package/src/features/assistant/index.ts +2 -0
  96. package/src/features/assistant/utils/__tests__/groupThreadsByBucket.spec.ts +24 -0
  97. package/src/features/assistant/utils/__tests__/resolveReferenceableModules.spec.ts +92 -0
  98. package/src/features/assistant/utils/groupThreadsByBucket.ts +26 -0
  99. package/src/features/assistant/utils/resolveReferenceableModules.ts +14 -0
  100. package/src/features/assistant-message/AssistantMessageModule.ts +28 -0
  101. package/src/features/assistant-message/components/MessageItem.tsx +60 -0
  102. package/src/features/assistant-message/components/MessageList.tsx +38 -0
  103. package/src/features/assistant-message/components/__tests__/MessageItem.spec.tsx +108 -0
  104. package/src/features/assistant-message/components/index.ts +2 -0
  105. package/src/features/assistant-message/components/parts/ReferenceBadges.tsx +46 -0
  106. package/src/features/assistant-message/components/parts/SuggestedFollowUps.tsx +52 -0
  107. package/src/features/assistant-message/components/parts/__tests__/ReferenceBadges.spec.tsx +59 -0
  108. package/src/features/assistant-message/components/parts/__tests__/SuggestedFollowUps.spec.tsx +29 -0
  109. package/src/features/assistant-message/data/AssistantMessage.ts +95 -0
  110. package/src/features/assistant-message/data/AssistantMessageInterface.ts +21 -0
  111. package/src/features/assistant-message/data/AssistantMessageService.ts +40 -0
  112. package/src/features/assistant-message/data/__tests__/AssistantMessage.spec.ts +158 -0
  113. package/src/features/assistant-message/data/index.ts +3 -0
  114. package/src/features/assistant-message/index.ts +2 -0
  115. package/src/features/rbac/components/RbacContainer.tsx +318 -49
  116. package/src/features/rbac/components/RbacPermissionPicker.tsx +144 -121
  117. package/src/features/rbac/contexts/RbacContext.tsx +209 -0
  118. package/src/features/rbac/contexts/index.ts +1 -0
  119. package/src/features/rbac/data/RbacMatrixModel.ts +84 -0
  120. package/src/features/rbac/data/RbacService.ts +61 -33
  121. package/src/features/rbac/data/RbacTypes.ts +28 -0
  122. package/src/features/rbac/data/index.ts +1 -0
  123. package/src/features/rbac/index.ts +1 -10
  124. package/src/features/rbac/rbac.module.ts +13 -0
  125. package/src/features/user/contexts/CurrentUserContext.tsx +5 -13
  126. package/src/features/user/contexts/__tests__/CurrentUserContext.spec.tsx +141 -0
  127. package/src/index.ts +4 -0
  128. package/dist/HowToInterface-BKhnkzBp.d.ts +0 -17
  129. package/dist/HowToInterface-Cj8OuQFf.d.mts +0 -17
  130. package/dist/ModulePathsInterface-BrdqgteS.d.mts +0 -31
  131. package/dist/ModulePathsInterface-DJKs7s_s.d.ts +0 -31
  132. package/dist/chunk-CV7UOUKQ.js.map +0 -1
  133. package/dist/chunk-F44ET4AC.mjs.map +0 -1
  134. package/dist/chunk-FKLP4NED.js.map +0 -1
  135. package/dist/chunk-OTZEXASK.js.map +0 -1
  136. package/dist/chunk-XI35ALWY.mjs.map +0 -1
  137. package/dist/useRbacState-C88O-5L8.d.ts +0 -77
  138. package/dist/useRbacState-mqYiRp3J.d.mts +0 -77
  139. package/src/features/rbac/components/RbacFeatureSection.tsx +0 -66
  140. package/src/features/rbac/components/RbacModuleTable.tsx +0 -121
  141. package/src/features/rbac/components/RbacToolbar.tsx +0 -40
  142. package/src/features/rbac/hooks/useRbacState.test.ts +0 -180
  143. package/src/features/rbac/hooks/useRbacState.ts +0 -319
  144. package/src/features/rbac/utils/RbacMigrationGenerator.test.ts +0 -124
  145. package/src/features/rbac/utils/RbacMigrationGenerator.ts +0 -184
  146. /package/dist/{BlockNoteEditor-7HAAXN3H.mjs.map → BlockNoteEditor-6CBDTVKV.mjs.map} +0 -0
@@ -15,7 +15,7 @@ var _chunk3ZPK4QOBjs = require('../chunk-3ZPK4QOB.js');
15
15
 
16
16
 
17
17
 
18
- var _chunkFKLP4NEDjs = require('../chunk-FKLP4NED.js');
18
+ var _chunk5IEWLLLDjs = require('../chunk-5IEWLLLD.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 _chunkFKLP4NEDjs.checkPermissionsFromServer.call(void 0, {
89
+ return _chunk5IEWLLLDjs.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 = _chunkFKLP4NEDjs.AuthService; exports.ServerCompanyService = _chunkFKLP4NEDjs.CompanyService; exports.ServerContentService = _chunkFKLP4NEDjs.ContentService; exports.ServerFeatureService = _chunkFKLP4NEDjs.FeatureService; exports.ServerJsonApiDelete = ServerJsonApiDelete; exports.ServerJsonApiGet = ServerJsonApiGet; exports.ServerJsonApiPatch = ServerJsonApiPatch; exports.ServerJsonApiPost = ServerJsonApiPost; exports.ServerJsonApiPut = ServerJsonApiPut; exports.ServerNotificationService = _chunkFKLP4NEDjs.NotificationService; exports.ServerPushService = _chunkFKLP4NEDjs.PushService; exports.ServerRoleService = _chunkFKLP4NEDjs.RoleService; exports.ServerS3Service = _chunkFKLP4NEDjs.S3Service; exports.ServerSession = ServerSession; exports.ServerUserService = _chunkFKLP4NEDjs.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 = _chunk5IEWLLLDjs.AuthService; exports.ServerCompanyService = _chunk5IEWLLLDjs.CompanyService; exports.ServerContentService = _chunk5IEWLLLDjs.ContentService; exports.ServerFeatureService = _chunk5IEWLLLDjs.FeatureService; exports.ServerJsonApiDelete = ServerJsonApiDelete; exports.ServerJsonApiGet = ServerJsonApiGet; exports.ServerJsonApiPatch = ServerJsonApiPatch; exports.ServerJsonApiPost = ServerJsonApiPost; exports.ServerJsonApiPut = ServerJsonApiPut; exports.ServerNotificationService = _chunk5IEWLLLDjs.NotificationService; exports.ServerPushService = _chunk5IEWLLLDjs.PushService; exports.ServerRoleService = _chunk5IEWLLLDjs.RoleService; exports.ServerS3Service = _chunk5IEWLLLDjs.S3Service; exports.ServerSession = ServerSession; exports.ServerUserService = _chunk5IEWLLLDjs.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-XI35ALWY.mjs";
18
+ } from "../chunk-BKM5U3DE.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.77.3",
3
+ "version": "1.79.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);
@@ -20,6 +20,8 @@ export * from "../features/auth/components";
20
20
  export * from "../features/company/components";
21
21
  export * from "../features/content/components";
22
22
  export * from "../features/how-to/components";
23
+ export * from "../features/assistant/components";
24
+ export * from "../features/assistant-message/components";
23
25
  export * from "../features/feature/components";
24
26
  export * from "../features/notification/components";
25
27
  export * from "../features/onboarding/components";
@@ -29,9 +31,6 @@ export * from "../features/user/components";
29
31
  export * from "../features/oauth/components";
30
32
  export * from "../features/waitlist/components";
31
33
  export { RbacContainer } from "../features/rbac/components/RbacContainer";
32
- export { RbacToolbar } from "../features/rbac/components/RbacToolbar";
33
- export { RbacFeatureSection } from "../features/rbac/components/RbacFeatureSection";
34
- export { RbacModuleTable } from "../features/rbac/components/RbacModuleTable";
35
34
  export { RbacPermissionCell } from "../features/rbac/components/RbacPermissionCell";
36
35
  export { RbacPermissionPicker } from "../features/rbac/components/RbacPermissionPicker";
37
36
 
@@ -4,6 +4,8 @@ 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";
8
+ export * from "../features/assistant/contexts/AssistantContext";
7
9
  export * from "./CommonContext";
8
10
  export * from "./HeaderChildrenContext";
9
11
  export * from "./HeaderLeftContentContext";
package/src/core/index.ts CHANGED
@@ -61,6 +61,10 @@ export * from "../features/content/content.module";
61
61
  export * from "../features/content/data";
62
62
  export * from "../features/how-to/HowToModule";
63
63
  export * from "../features/how-to/data";
64
+ export * from "../features/assistant/AssistantModule";
65
+ export * from "../features/assistant/data";
66
+ export * from "../features/assistant-message/AssistantMessageModule";
67
+ export * from "../features/assistant-message/data";
64
68
  export * from "../features/feature/data";
65
69
  export * from "../features/feature/feature.module";
66
70
  export * from "../features/module";
@@ -16,6 +16,8 @@ export interface FoundationModuleDefinitions {
16
16
  Module: ModuleWithPermissions;
17
17
  Content: ModuleWithPermissions;
18
18
  HowTo: ModuleWithPermissions;
19
+ Assistant: ModuleWithPermissions;
20
+ AssistantMessage: ModuleWithPermissions;
19
21
  // Billing modules - READ: all users, UPDATE: CompanyAdministrator, ADMIN: Administrator
20
22
  Billing: ModuleWithPermissions;
21
23
  StripeCustomer: ModuleWithPermissions;
@@ -52,6 +54,7 @@ export interface FoundationModuleDefinitions {
52
54
  // RBAC modules
53
55
  PermissionMapping: ModuleWithPermissions;
54
56
  ModulePaths: ModuleWithPermissions;
57
+ RbacMatrix: ModuleWithPermissions;
55
58
  // Audit modules
56
59
  AuditLog: ModuleWithPermissions;
57
60
  }
@@ -171,6 +174,13 @@ class ModuleRegistryClass {
171
174
 
172
175
  return response;
173
176
  }
177
+
178
+ getAll(): ApiRequestDataTypeInterface[] {
179
+ if (this._modules.size === 0) {
180
+ tryBootstrap();
181
+ }
182
+ return Array.from(this._modules.values());
183
+ }
174
184
  }
175
185
 
176
186
  export const ModuleRegistry = new ModuleRegistryClass();
@@ -0,0 +1,19 @@
1
+ import { BotIcon } from "lucide-react";
2
+ import { createJsonApiInclusion } from "../../core";
3
+ import { ModuleFactory } from "../../permissions";
4
+ import { Assistant } from "./data/Assistant";
5
+
6
+ export const AssistantModule = (factory: ModuleFactory) =>
7
+ factory({
8
+ pageUrl: "/assistants",
9
+ name: "assistants",
10
+ model: Assistant,
11
+ moduleId: "2b39fd68-6a41-4f73-a2d0-4c8e8e3f9a42",
12
+ icon: BotIcon,
13
+ identifier: ["title"],
14
+ inclusions: {
15
+ lists: {
16
+ fields: [createJsonApiInclusion("assistants", [`title`])],
17
+ },
18
+ },
19
+ });
@@ -0,0 +1,56 @@
1
+ "use client";
2
+
3
+ import { RoundPageContainer } from "../../../../components/containers/RoundPageContainer";
4
+ import { Modules } from "../../../../core";
5
+ import { useAssistantContext } from "../../contexts/AssistantContext";
6
+ import { AssistantSidebar } from "../parts/AssistantSidebar";
7
+ import { AssistantEmptyState } from "../parts/AssistantEmptyState";
8
+ import { AssistantThreadHeader } from "../parts/AssistantThreadHeader";
9
+ import { AssistantThread } from "../parts/AssistantThread";
10
+ import { AssistantComposer } from "../parts/AssistantComposer";
11
+
12
+ export function AssistantContainer() {
13
+ const ctx = useAssistantContext();
14
+ const showThread = !!ctx.assistant || ctx.sending || ctx.messages.length > 0;
15
+
16
+ return (
17
+ <RoundPageContainer module={Modules.Assistant} fullWidth>
18
+ <div className="bg-background flex h-full w-full overflow-hidden rounded-lg border">
19
+ <AssistantSidebar
20
+ threads={ctx.threads}
21
+ activeId={ctx.assistant?.id}
22
+ onSelect={ctx.selectThread}
23
+ onNew={ctx.startNew}
24
+ />
25
+ <main className="flex flex-1 flex-col">
26
+ {!showThread ? (
27
+ <AssistantEmptyState onSend={ctx.sendMessage} />
28
+ ) : (
29
+ <>
30
+ {ctx.assistant ? (
31
+ <AssistantThreadHeader
32
+ assistant={ctx.assistant}
33
+ onRename={(title) => ctx.renameThread(ctx.assistant!.id, title)}
34
+ onDelete={() => ctx.deleteThread(ctx.assistant!.id)}
35
+ />
36
+ ) : (
37
+ <div className="flex items-center justify-between border-b px-5 py-3" aria-hidden>
38
+ <div className="h-5" />
39
+ </div>
40
+ )}
41
+ <AssistantThread
42
+ messages={ctx.messages}
43
+ sending={ctx.sending}
44
+ status={ctx.status}
45
+ onSelectFollowUp={ctx.sendMessage}
46
+ failedMessageIds={ctx.failedMessageIds}
47
+ onRetry={ctx.retrySend}
48
+ />
49
+ <AssistantComposer onSend={ctx.sendMessage} disabled={ctx.sending} />
50
+ </>
51
+ )}
52
+ </main>
53
+ </div>
54
+ </RoundPageContainer>
55
+ );
56
+ }
@@ -0,0 +1,101 @@
1
+ vi.mock("../../../../../contexts/SocketContext", () => ({
2
+ useSocketContext: () => ({ socket: null, isConnected: false }),
3
+ }));
4
+
5
+ vi.mock("../../../../../components/containers/RoundPageContainer", () => ({
6
+ RoundPageContainer: ({ children }: { children: React.ReactNode }) => <>{children}</>,
7
+ }));
8
+
9
+ import { describe, it, expect, vi, beforeAll, beforeEach } from "vitest";
10
+ import { render, screen, waitFor } from "@testing-library/react";
11
+ import { AssistantProvider } from "../../../contexts/AssistantContext";
12
+ import { AssistantService } from "../../../data/AssistantService";
13
+ import type { JsonApiHydratedDataInterface } from "../../../../../core";
14
+ import { AssistantContainer } from "../AssistantContainer";
15
+ import { ModuleRegistry } from "../../../../../core/registry/ModuleRegistry";
16
+ import { DataClassRegistry } from "../../../../../core/registry/DataClassRegistry";
17
+ import { Assistant } from "../../../data/Assistant";
18
+ import { AssistantMessage } from "../../../../assistant-message/data/AssistantMessage";
19
+
20
+ beforeAll(() => {
21
+ Element.prototype.scrollIntoView = vi.fn();
22
+ const assistantModule = { name: "assistants", model: Assistant } as any;
23
+ const assistantMessageModule = { name: "assistant-messages", model: AssistantMessage } as any;
24
+ DataClassRegistry.registerObjectClass(assistantModule, Assistant);
25
+ DataClassRegistry.registerObjectClass(assistantMessageModule, AssistantMessage);
26
+ ModuleRegistry.register("Assistant" as any, assistantModule as any);
27
+ ModuleRegistry.register("AssistantMessage" as any, assistantMessageModule as any);
28
+ });
29
+
30
+ beforeEach(() => {
31
+ AssistantService.findMany = vi.fn().mockResolvedValue([]);
32
+ });
33
+
34
+ function buildAssistantDehydrated({
35
+ id,
36
+ title = "Stub",
37
+ }: {
38
+ id: string;
39
+ title?: string;
40
+ }): JsonApiHydratedDataInterface {
41
+ return {
42
+ jsonApi: {
43
+ type: "assistants",
44
+ id,
45
+ attributes: { title, messageCount: 0 },
46
+ },
47
+ included: [],
48
+ };
49
+ }
50
+
51
+ describe("AssistantContainer", () => {
52
+ it("shows empty state when no assistant is active", async () => {
53
+ render(
54
+ <AssistantProvider>
55
+ <AssistantContainer />
56
+ </AssistantProvider>,
57
+ );
58
+ // AssistantEmptyState renders heading with translation key
59
+ await waitFor(() =>
60
+ expect(screen.getByRole("heading", { name: "features.assistant.empty_state.title" })).toBeInTheDocument(),
61
+ );
62
+ });
63
+
64
+ it("shows thread header + composer when an assistant is active", async () => {
65
+ const active = buildAssistantDehydrated({ id: "a1", title: "T" });
66
+ render(
67
+ <AssistantProvider dehydratedAssistant={active}>
68
+ <AssistantContainer />
69
+ </AssistantProvider>,
70
+ );
71
+ // Header shows the assistant title
72
+ expect(screen.getByText("T")).toBeInTheDocument();
73
+ // Composer has a textarea
74
+ expect(screen.getByRole("textbox")).toBeInTheDocument();
75
+ });
76
+
77
+ it("shows thread view (not empty state) when sending a first message before assistant resolves", async () => {
78
+ // Keep `AssistantService.create` pending so the assistant is never set.
79
+ AssistantService.create = vi.fn().mockImplementation(() => new Promise(() => {}));
80
+
81
+ render(
82
+ <AssistantProvider>
83
+ <AssistantContainer />
84
+ </AssistantProvider>,
85
+ );
86
+
87
+ // Kick off a send via the composer. The AssistantEmptyState renders its own composer.
88
+ const textarea = await screen.findByRole("textbox");
89
+ const user = (await import("@testing-library/user-event")).default.setup();
90
+ await user.type(textarea, "first one");
91
+ await user.keyboard("{Enter}");
92
+
93
+ // Empty-state title disappears; optimistic user bubble visible.
94
+ await waitFor(() => {
95
+ expect(screen.queryByRole("heading", { name: "features.assistant.empty_state.title" })).not.toBeInTheDocument();
96
+ });
97
+ expect(screen.getByText("first one")).toBeInTheDocument();
98
+ // Status line is showing ("thinking…" translation key renders literally in tests).
99
+ expect(screen.getByText("features.assistant.thinking")).toBeInTheDocument();
100
+ });
101
+ });
@@ -0,0 +1 @@
1
+ export * from "./containers/AssistantContainer";
@@ -0,0 +1,56 @@
1
+ "use client";
2
+
3
+ import { useState, type KeyboardEvent } from "react";
4
+ import { useTranslations } from "next-intl";
5
+ import { ArrowUp } from "lucide-react";
6
+ import { Button, Textarea } from "../../../../shadcnui";
7
+
8
+ interface Props {
9
+ onSend: (content: string) => Promise<void>;
10
+ disabled?: boolean;
11
+ value?: string;
12
+ onValueChange?: (v: string) => void;
13
+ }
14
+
15
+ export function AssistantComposer({ onSend, disabled, value: controlled, onValueChange }: Props) {
16
+ const t = useTranslations();
17
+ const [internal, setInternal] = useState("");
18
+ const value = controlled ?? internal;
19
+ const setValue = onValueChange ?? setInternal;
20
+
21
+ const canSend = value.trim().length > 0 && !disabled;
22
+
23
+ const submit = async () => {
24
+ if (!canSend) return;
25
+ const payload = value.trim();
26
+ setValue("");
27
+ await onSend(payload);
28
+ };
29
+
30
+ const onKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
31
+ if (e.key === "Enter" && !e.shiftKey) {
32
+ e.preventDefault();
33
+ void submit();
34
+ }
35
+ };
36
+
37
+ return (
38
+ <div className="flex flex-col gap-1 border-t p-4">
39
+ <div className="bg-muted/30 flex items-end gap-2 rounded-lg border p-2">
40
+ <Textarea
41
+ value={value}
42
+ onChange={(e) => setValue(e.target.value)}
43
+ onKeyDown={onKeyDown}
44
+ placeholder={t("features.assistant.composer_placeholder")}
45
+ disabled={disabled}
46
+ rows={2}
47
+ className="min-h-[48px] resize-none border-0 bg-transparent focus-visible:ring-0"
48
+ />
49
+ <Button onClick={submit} disabled={!canSend} size="sm" className="h-8">
50
+ <ArrowUp className="mr-1 h-4 w-4" /> {t("ui.buttons.save")}
51
+ </Button>
52
+ </div>
53
+ <div className="text-muted-foreground text-right text-xs">{t("features.assistant.keyboard_hint")}</div>
54
+ </div>
55
+ );
56
+ }
@@ -0,0 +1,47 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { useTranslations } from "next-intl";
5
+ import { Sparkles } from "lucide-react";
6
+ import { AssistantComposer } from "./AssistantComposer";
7
+
8
+ interface Props {
9
+ onSend: (content: string) => Promise<void>;
10
+ }
11
+
12
+ const STARTER_KEYS = ["a", "b", "c", "d"] as const;
13
+
14
+ export function AssistantEmptyState({ onSend }: Props) {
15
+ const t = useTranslations();
16
+ const [draft, setDraft] = useState("");
17
+
18
+ return (
19
+ <div className="flex h-full w-full items-center justify-center p-10">
20
+ <div className="flex w-full max-w-2xl flex-col gap-6">
21
+ <div className="text-center">
22
+ <div className="mx-auto mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-gradient-to-br from-blue-400 to-violet-500 text-white">
23
+ <Sparkles className="h-5 w-5" />
24
+ </div>
25
+ <h3 className="text-foreground text-xl font-semibold">{t("features.assistant.empty_state.title")}</h3>
26
+ <p className="text-muted-foreground mt-1 text-sm">{t("features.assistant.empty_state.subtitle")}</p>
27
+ </div>
28
+ <AssistantComposer value={draft} onValueChange={setDraft} onSend={onSend} />
29
+ <div className="grid grid-cols-2 gap-2">
30
+ {STARTER_KEYS.map((k) => {
31
+ const text = t(`features.assistant.starters.${k}`);
32
+ return (
33
+ <button
34
+ key={k}
35
+ type="button"
36
+ className="border-border bg-muted/30 hover:bg-muted rounded-lg border p-3 text-left text-sm"
37
+ onClick={() => setDraft(text)}
38
+ >
39
+ {text}
40
+ </button>
41
+ );
42
+ })}
43
+ </div>
44
+ </div>
45
+ </div>
46
+ );
47
+ }
@@ -0,0 +1,64 @@
1
+ "use client";
2
+
3
+ import { useTranslations } from "next-intl";
4
+ import { Plus } from "lucide-react";
5
+ import { Button } from "../../../../shadcnui";
6
+ import type { AssistantInterface } from "../../data/AssistantInterface";
7
+ import { groupThreadsByBucket } from "../../utils/groupThreadsByBucket";
8
+
9
+ interface Props {
10
+ threads: AssistantInterface[];
11
+ activeId?: string;
12
+ onSelect: (id: string) => void;
13
+ onNew: () => void;
14
+ }
15
+
16
+ export function AssistantSidebar({ threads, activeId, onSelect, onNew }: Props) {
17
+ const t = useTranslations();
18
+ const groups = groupThreadsByBucket(threads);
19
+
20
+ const renderSection = (label: string, items: AssistantInterface[]) => {
21
+ if (items.length === 0) return null;
22
+ return (
23
+ <div className="mb-2">
24
+ <div className="text-muted-foreground px-2 py-1 text-[10px] font-semibold uppercase tracking-wider">
25
+ {label}
26
+ </div>
27
+ {items.map((thread) => (
28
+ <button
29
+ key={thread.id}
30
+ type="button"
31
+ onClick={() => onSelect(thread.id)}
32
+ className={
33
+ "block w-full truncate rounded-md px-2 py-1.5 text-left text-sm " +
34
+ (activeId === thread.id ? "bg-primary/10 text-primary" : "hover:bg-muted text-foreground")
35
+ }
36
+ >
37
+ {thread.title}
38
+ </button>
39
+ ))}
40
+ </div>
41
+ );
42
+ };
43
+
44
+ return (
45
+ <aside className="bg-muted/30 flex w-64 flex-col border-r">
46
+ <div className="border-b p-3">
47
+ <Button onClick={onNew} className="w-full" size="sm">
48
+ <Plus className="mr-1 h-4 w-4" /> {t("features.assistant.new")}
49
+ </Button>
50
+ </div>
51
+ <div className="flex-1 overflow-y-auto p-2">
52
+ {threads.length === 0 ? (
53
+ <div className="text-muted-foreground mt-6 text-center text-xs">{t("features.assistant.empty_sidebar")}</div>
54
+ ) : (
55
+ <>
56
+ {renderSection(t("features.assistant.bucket_today"), groups.today)}
57
+ {renderSection(t("features.assistant.bucket_week"), groups.thisWeek)}
58
+ {renderSection(t("features.assistant.bucket_earlier"), groups.earlier)}
59
+ </>
60
+ )}
61
+ </div>
62
+ </aside>
63
+ );
64
+ }
@@ -0,0 +1,19 @@
1
+ "use client";
2
+
3
+ import { useTranslations } from "next-intl";
4
+ import { Loader2 } from "lucide-react";
5
+
6
+ interface Props {
7
+ status?: string;
8
+ }
9
+
10
+ export function AssistantStatusLine({ status }: Props) {
11
+ const t = useTranslations();
12
+ const text = status ?? t("features.assistant.thinking");
13
+ return (
14
+ <div className="text-muted-foreground flex items-center gap-2 px-4 py-2 text-sm">
15
+ <Loader2 className="h-4 w-4 animate-spin" />
16
+ <span>{text}</span>
17
+ </div>
18
+ );
19
+ }
@@ -0,0 +1,36 @@
1
+ "use client";
2
+
3
+ import { useEffect, useRef } from "react";
4
+ import type { AssistantMessageInterface } from "../../../assistant-message/data/AssistantMessageInterface";
5
+ import { MessageList } from "../../../assistant-message/components/MessageList";
6
+ import { AssistantStatusLine } from "./AssistantStatusLine";
7
+
8
+ interface Props {
9
+ messages: AssistantMessageInterface[];
10
+ sending: boolean;
11
+ status?: string;
12
+ onSelectFollowUp: (q: string) => void;
13
+ failedMessageIds?: Set<string>;
14
+ onRetry?: (tempId: string) => void;
15
+ }
16
+
17
+ export function AssistantThread({ messages, sending, status, onSelectFollowUp, failedMessageIds, onRetry }: Props) {
18
+ const endRef = useRef<HTMLDivElement>(null);
19
+
20
+ useEffect(() => {
21
+ endRef.current?.scrollIntoView({ behavior: "smooth" });
22
+ }, [messages.length, sending]);
23
+
24
+ return (
25
+ <div className="flex-1 overflow-y-auto px-6 py-5">
26
+ <MessageList
27
+ messages={messages}
28
+ onSelectFollowUp={onSelectFollowUp}
29
+ failedMessageIds={failedMessageIds}
30
+ onRetry={onRetry}
31
+ />
32
+ {sending && <AssistantStatusLine status={status} />}
33
+ <div ref={endRef} />
34
+ </div>
35
+ );
36
+ }