@carlonicora/nextjs-jsonapi 1.107.1 → 1.109.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 (168) hide show
  1. package/dist/{AssistantInterface-BYgI5z1-.d.mts → AssistantInterface-B1c8FhGA.d.mts} +2 -0
  2. package/dist/{AssistantInterface-DfDcz0gJ.d.ts → AssistantInterface-BBUHxOCd.d.ts} +2 -0
  3. package/dist/{AssistantMessageInterface-BpEhx2pC.d.ts → AssistantMessageInterface-Cs1yb-gF.d.ts} +3 -1
  4. package/dist/{AssistantMessageInterface-DJ3Me16Y.d.mts → AssistantMessageInterface-DQ3mH5L8.d.mts} +3 -1
  5. package/dist/{AuthComponent-B6DIk8Vf.d.ts → AuthComponent-Cd7lcYif.d.ts} +1 -1
  6. package/dist/{AuthComponent-BKI0ZbtD.d.mts → AuthComponent-DdxCFgUZ.d.mts} +1 -1
  7. package/dist/{BlockNoteEditor-RWRVIEZC.js → BlockNoteEditor-3XYBZLWO.js} +20 -19
  8. package/dist/BlockNoteEditor-3XYBZLWO.js.map +1 -0
  9. package/dist/{BlockNoteEditor-7TSK7PNG.mjs → BlockNoteEditor-EBFZG7AL.mjs} +5 -4
  10. package/dist/{BlockNoteEditor-7TSK7PNG.mjs.map → BlockNoteEditor-EBFZG7AL.mjs.map} +1 -1
  11. package/dist/{auth.interface-BBUgMZzs.d.ts → auth.interface-8b601idJ.d.ts} +1 -1
  12. package/dist/{auth.interface-XYEREOD6.d.mts → auth.interface-CXBF8Mhi.d.mts} +1 -1
  13. package/dist/billing/index.js +347 -346
  14. package/dist/billing/index.js.map +1 -1
  15. package/dist/billing/index.mjs +4 -3
  16. package/dist/billing/index.mjs.map +1 -1
  17. package/dist/chunk-3J7RQBF3.js +123 -0
  18. package/dist/chunk-3J7RQBF3.js.map +1 -0
  19. package/dist/{chunk-VLDLERJN.js → chunk-7E3O52U5.js} +15 -8
  20. package/dist/chunk-7E3O52U5.js.map +1 -0
  21. package/dist/{chunk-RXXZGPC3.js → chunk-CFI4WZ5R.js} +159 -113
  22. package/dist/chunk-CFI4WZ5R.js.map +1 -0
  23. package/dist/chunk-CQID6RCF.mjs +38 -0
  24. package/dist/chunk-CQID6RCF.mjs.map +1 -0
  25. package/dist/{chunk-WSOPEIRP.mjs → chunk-CRTVAQEK.mjs} +46 -27
  26. package/dist/chunk-CRTVAQEK.mjs.map +1 -0
  27. package/dist/{chunk-N3NVIPSU.mjs → chunk-MSNNAHDB.mjs} +129 -83
  28. package/dist/{chunk-N3NVIPSU.mjs.map → chunk-MSNNAHDB.mjs.map} +1 -1
  29. package/dist/chunk-MZTKPPET.mjs +123 -0
  30. package/dist/chunk-MZTKPPET.mjs.map +1 -0
  31. package/dist/{chunk-2IRWQVG4.js → chunk-UHO3KUUH.js} +842 -823
  32. package/dist/chunk-UHO3KUUH.js.map +1 -0
  33. package/dist/{chunk-CFECWLHH.mjs → chunk-UOYIWJEJ.mjs} +10 -3
  34. package/dist/chunk-UOYIWJEJ.mjs.map +1 -0
  35. package/dist/chunk-YQQHAFBS.js +38 -0
  36. package/dist/chunk-YQQHAFBS.js.map +1 -0
  37. package/dist/client/index.d.mts +8 -16
  38. package/dist/client/index.d.ts +8 -16
  39. package/dist/client/index.js +5 -4
  40. package/dist/client/index.js.map +1 -1
  41. package/dist/client/index.mjs +4 -3
  42. package/dist/components/index.d.mts +8 -7
  43. package/dist/components/index.d.ts +8 -7
  44. package/dist/components/index.js +5 -4
  45. package/dist/components/index.js.map +1 -1
  46. package/dist/components/index.mjs +4 -3
  47. package/dist/{config-CLQynoaa.d.ts → config-CN23v3eJ.d.ts} +4 -1
  48. package/dist/{config-k61pe_o2.d.mts → config-gh88Qn4h.d.mts} +4 -1
  49. package/dist/contexts/index.d.mts +18 -7
  50. package/dist/contexts/index.d.ts +18 -7
  51. package/dist/contexts/index.js +5 -4
  52. package/dist/contexts/index.js.map +1 -1
  53. package/dist/contexts/index.mjs +4 -3
  54. package/dist/core/index.d.mts +44 -11
  55. package/dist/core/index.d.ts +44 -11
  56. package/dist/core/index.js +2 -2
  57. package/dist/core/index.mjs +1 -1
  58. package/dist/features/help/index.css +29 -0
  59. package/dist/features/help/index.css.map +1 -0
  60. package/dist/features/help/index.d.mts +115 -0
  61. package/dist/features/help/index.d.ts +115 -0
  62. package/dist/features/help/index.js +532 -0
  63. package/dist/features/help/index.js.map +1 -0
  64. package/dist/features/help/index.mjs +532 -0
  65. package/dist/features/help/index.mjs.map +1 -0
  66. package/dist/features/help/server/createHelpAssetRouteHandler.d.mts +11 -0
  67. package/dist/features/help/server/createHelpAssetRouteHandler.d.ts +11 -0
  68. package/dist/features/help/server/createHelpAssetRouteHandler.js +43 -0
  69. package/dist/features/help/server/createHelpAssetRouteHandler.js.map +1 -0
  70. package/dist/features/help/server/createHelpAssetRouteHandler.mjs +43 -0
  71. package/dist/features/help/server/createHelpAssetRouteHandler.mjs.map +1 -0
  72. package/dist/features/help/server.d.mts +71 -0
  73. package/dist/features/help/server.d.ts +71 -0
  74. package/dist/features/help/server.js +123 -0
  75. package/dist/features/help/server.js.map +1 -0
  76. package/dist/features/help/server.mjs +123 -0
  77. package/dist/features/help/server.mjs.map +1 -0
  78. package/dist/help-content-config.interface-B9L02u9i.d.mts +50 -0
  79. package/dist/help-content-config.interface-B9L02u9i.d.ts +50 -0
  80. package/dist/index.d.mts +10 -8
  81. package/dist/index.d.ts +10 -8
  82. package/dist/index.js +4 -3
  83. package/dist/index.js.map +1 -1
  84. package/dist/index.mjs +3 -2
  85. package/dist/{notification.interface-aLEJbA_g.d.ts → notification.interface-C1T1C2ee.d.ts} +1 -100
  86. package/dist/{notification.interface-DLZGtV7Z.d.mts → notification.interface-DIxR23eS.d.mts} +1 -100
  87. package/dist/{s3.service-CVgLWaDc.d.mts → s3.service-0BTClOYO.d.mts} +2 -2
  88. package/dist/{s3.service-SLlX0Zbz.d.ts → s3.service-CT27Fm1s.d.ts} +2 -2
  89. package/dist/server/index.d.mts +4 -3
  90. package/dist/server/index.d.ts +4 -3
  91. package/dist/server/index.js +3 -3
  92. package/dist/server/index.mjs +1 -1
  93. package/dist/types-CQSjy7et.d.mts +101 -0
  94. package/dist/types-DHOxe8rc.d.ts +101 -0
  95. package/dist/usePageUrlGenerator-tjq2mlDV.d.ts +14 -0
  96. package/dist/usePageUrlGenerator-uOnyJ6j2.d.mts +14 -0
  97. package/dist/{useSocket-BkxHHujj.d.mts → useSocket-B1fMIr17.d.mts} +1 -1
  98. package/dist/{useSocket-CMDjWFYm.d.ts → useSocket-BdJTBXKv.d.ts} +1 -1
  99. package/package.json +20 -1
  100. package/src/client/config.ts +9 -1
  101. package/src/core/registry/helpStore.ts +45 -0
  102. package/src/features/assistant/contexts/AssistantContext.tsx +35 -19
  103. package/src/features/assistant/contexts/__tests__/AssistantContext.spec.tsx +66 -6
  104. package/src/features/assistant/data/Assistant.ts +2 -0
  105. package/src/features/assistant/data/AssistantInterface.ts +2 -0
  106. package/src/features/assistant/data/AssistantService.ts +18 -8
  107. package/src/features/assistant-message/components/parts/MessageSourcesPanel.tsx +6 -4
  108. package/src/features/assistant-message/components/parts/tabs/ContentsTab.tsx +5 -1
  109. package/src/features/assistant-message/components/parts/tabs/ReferencesTab.tsx +2 -1
  110. package/src/features/assistant-message/data/AssistantMessage.ts +27 -1
  111. package/src/features/assistant-message/data/AssistantMessageInterface.ts +1 -0
  112. package/src/features/assistant-message/data/__tests__/AssistantMessage.spec.ts +7 -3
  113. package/src/features/help/components/HelpArticleBody.tsx +54 -0
  114. package/src/features/help/components/HelpAskAi.tsx +36 -0
  115. package/src/features/help/components/HelpAssistantSheet.tsx +53 -0
  116. package/src/features/help/components/HelpHeader.tsx +40 -0
  117. package/src/features/help/components/HelpHint.tsx +77 -0
  118. package/src/features/help/components/HelpSearchResultRow.tsx +51 -0
  119. package/src/features/help/components/HelpSideNav.tsx +84 -0
  120. package/src/features/help/components/HelpTOC.tsx +49 -0
  121. package/src/features/help/components/__tests__/HelpAskAi.spec.tsx +68 -0
  122. package/src/features/help/components/__tests__/HelpAssistantSheet.spec.tsx +36 -0
  123. package/src/features/help/components/__tests__/HelpHint.spec.tsx +50 -0
  124. package/src/features/help/components/__tests__/HelpSearchResultRow.spec.tsx +59 -0
  125. package/src/features/help/components/__tests__/HelpSideNav.spec.tsx +52 -0
  126. package/src/features/help/components/mdx/Callout.tsx +21 -0
  127. package/src/features/help/components/mdx/EntityRef.tsx +18 -0
  128. package/src/features/help/components/mdx/KeyBinding.tsx +6 -0
  129. package/src/features/help/components/mdx/Related.tsx +33 -0
  130. package/src/features/help/components/mdx/Screenshot.tsx +9 -0
  131. package/src/features/help/components/mdx/Steps.tsx +21 -0
  132. package/src/features/help/components/mdx/Video.tsx +8 -0
  133. package/src/features/help/components/mdx/mdx-server-components.ts +23 -0
  134. package/src/features/help/components/mdx/mdxComponents.ts +9 -0
  135. package/src/features/help/contexts/HelpContext.spec.tsx +28 -0
  136. package/src/features/help/contexts/HelpContext.tsx +24 -0
  137. package/src/features/help/hooks/useHelp.ts +1 -0
  138. package/src/features/help/hooks/useHelpArticle.ts +7 -0
  139. package/src/features/help/hooks/useHelpFilter.ts +27 -0
  140. package/src/features/help/hooks/useHelpManifest.ts +5 -0
  141. package/src/features/help/i18n-keys.ts +34 -0
  142. package/src/features/help/index.ts +27 -0
  143. package/src/features/help/interfaces/help-content-config.interface.ts +17 -0
  144. package/src/features/help/server/__tests__/createHelpAssetRouteHandler.spec.ts +43 -0
  145. package/src/features/help/server/createHelpAssetRouteHandler.ts +35 -0
  146. package/src/features/help/server/generateHelpArticleMetadata.ts +18 -0
  147. package/src/features/help/server/generateHelpArticleStaticParams.ts +7 -0
  148. package/src/features/help/server/generateHelpModeStaticParams.ts +5 -0
  149. package/src/features/help/server/getHelpContent.ts +17 -0
  150. package/src/features/help/server/index.ts +8 -0
  151. package/src/features/help/server/serializeHelpArticle.tsx +46 -0
  152. package/src/features/help/server-entry.ts +20 -0
  153. package/src/features/help/types/help-article.types.ts +37 -0
  154. package/src/features/help/utils/__tests__/helpNavigation.spec.ts +70 -0
  155. package/src/features/help/utils/articleUrl.ts +13 -0
  156. package/src/features/help/utils/helpNavigation.ts +29 -0
  157. package/src/features/how-to/HowToModule.ts +1 -1
  158. package/src/features/how-to/data/HowTo.ts +21 -3
  159. package/src/features/how-to/data/HowToInterface.ts +1 -0
  160. package/src/index.ts +4 -0
  161. package/src/shadcnui/ui/context-menu.tsx +3 -1
  162. package/src/shadcnui/ui/popover.tsx +3 -1
  163. package/dist/BlockNoteEditor-RWRVIEZC.js.map +0 -1
  164. package/dist/chunk-2IRWQVG4.js.map +0 -1
  165. package/dist/chunk-CFECWLHH.mjs.map +0 -1
  166. package/dist/chunk-RXXZGPC3.js.map +0 -1
  167. package/dist/chunk-VLDLERJN.js.map +0 -1
  168. package/dist/chunk-WSOPEIRP.mjs.map +0 -1
@@ -0,0 +1,101 @@
1
+ import { LucideIcon } from 'lucide-react';
2
+ import { A as ApiRequestDataTypeInterface, F as FieldSelector } from './ApiRequestDataTypeInterface-CYEcRUrh.js';
3
+
4
+ /**
5
+ * Permission actions
6
+ */
7
+ declare enum Action {
8
+ Read = "read",
9
+ Create = "create",
10
+ Update = "update",
11
+ Delete = "delete"
12
+ }
13
+ /**
14
+ * Generic permission check type.
15
+ * Can be a boolean or a function that checks permissions dynamically.
16
+ * @template T - The data type being checked
17
+ * @template U - The user type (defaults to PermissionUser)
18
+ */
19
+ type PermissionCheck<T, U = PermissionUser> = boolean | ((user?: U | string, data?: T) => boolean);
20
+ /**
21
+ * Page URL configuration for modules
22
+ */
23
+ type PageUrl = {
24
+ pageUrl?: string;
25
+ };
26
+ /**
27
+ * Module permission definition wrapper
28
+ */
29
+ type ModulePermissionDefinition<T> = {
30
+ interface: T;
31
+ };
32
+ /**
33
+ * Base module definition
34
+ */
35
+ type ModuleDefinition = {
36
+ pageUrl?: string;
37
+ name: string;
38
+ model: any;
39
+ feature?: string;
40
+ moduleId?: string;
41
+ };
42
+ /**
43
+ * Permission configuration for a module.
44
+ * Can be a boolean (allow/deny all) or a string path for dynamic checks.
45
+ */
46
+ interface PermissionConfig {
47
+ create: boolean | string;
48
+ read: boolean | string;
49
+ update: boolean | string;
50
+ delete: boolean | string;
51
+ }
52
+ /**
53
+ * Generic interface for a module that has permissions.
54
+ * Apps should ensure their Module class implements this.
55
+ */
56
+ interface PermissionModule {
57
+ id: string;
58
+ permissions: PermissionConfig;
59
+ }
60
+ /**
61
+ * Generic interface for a user that has modules with permissions.
62
+ * Apps should ensure their User class implements this.
63
+ */
64
+ interface PermissionUser {
65
+ id: string;
66
+ modules: PermissionModule[];
67
+ }
68
+ /**
69
+ * Module definition with permissions - extends ApiRequestDataTypeInterface
70
+ */
71
+ type ModuleWithPermissions = ApiRequestDataTypeInterface & {
72
+ pageUrl?: string;
73
+ feature?: string;
74
+ moduleId?: string;
75
+ icon?: LucideIcon;
76
+ inclusions?: Record<string, {
77
+ types?: string[];
78
+ fields?: FieldSelector<any>[];
79
+ include?: string[];
80
+ }>;
81
+ };
82
+ /**
83
+ * Factory type for creating module definitions
84
+ */
85
+ type ModuleFactory = (params: {
86
+ pageUrl?: string;
87
+ name: string;
88
+ cache?: string | "days" | "default" | "hours" | "max" | "minutes" | "seconds" | "weeks";
89
+ model: any;
90
+ feature?: string;
91
+ moduleId?: string;
92
+ icon?: LucideIcon;
93
+ identifier?: string[];
94
+ inclusions?: Record<string, {
95
+ types?: string[];
96
+ fields?: FieldSelector<any>[];
97
+ include?: string[];
98
+ }>;
99
+ }) => ModuleWithPermissions;
100
+
101
+ export { Action as A, type ModuleWithPermissions as M, type PageUrl as P, type PermissionCheck as a, type ModulePermissionDefinition as b, type ModuleDefinition as c, type PermissionConfig as d, type PermissionModule as e, type PermissionUser as f, type ModuleFactory as g };
@@ -0,0 +1,14 @@
1
+ import { P as PageUrl } from './types-DHOxe8rc.js';
2
+
3
+ declare function usePageUrlGenerator(): (params: {
4
+ page?: PageUrl | string;
5
+ id?: string;
6
+ childPage?: PageUrl | string;
7
+ childId?: string;
8
+ additionalParameters?: {
9
+ [key: string]: string | string[] | undefined;
10
+ };
11
+ language?: string;
12
+ }) => string;
13
+
14
+ export { usePageUrlGenerator as u };
@@ -0,0 +1,14 @@
1
+ import { P as PageUrl } from './types-CQSjy7et.mjs';
2
+
3
+ declare function usePageUrlGenerator(): (params: {
4
+ page?: PageUrl | string;
5
+ id?: string;
6
+ childPage?: PageUrl | string;
7
+ childId?: string;
8
+ additionalParameters?: {
9
+ [key: string]: string | string[] | undefined;
10
+ };
11
+ language?: string;
12
+ }) => string;
13
+
14
+ export { usePageUrlGenerator as u };
@@ -1,4 +1,4 @@
1
- import { N as NotificationInterface } from './notification.interface-DLZGtV7Z.mjs';
1
+ import { N as NotificationInterface } from './notification.interface-DIxR23eS.mjs';
2
2
 
3
3
  interface UseSocketOptions {
4
4
  token: string;
@@ -1,4 +1,4 @@
1
- import { N as NotificationInterface } from './notification.interface-aLEJbA_g.js';
1
+ import { N as NotificationInterface } from './notification.interface-C1T1C2ee.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.107.1",
3
+ "version": "1.109.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",
@@ -52,6 +52,21 @@
52
52
  "types": "./dist/billing/index.d.ts",
53
53
  "import": "./dist/billing/index.mjs",
54
54
  "require": "./dist/billing/index.js"
55
+ },
56
+ "./help-asset-route": {
57
+ "types": "./dist/features/help/server/createHelpAssetRouteHandler.d.ts",
58
+ "import": "./dist/features/help/server/createHelpAssetRouteHandler.mjs",
59
+ "require": "./dist/features/help/server/createHelpAssetRouteHandler.js"
60
+ },
61
+ "./help": {
62
+ "types": "./dist/features/help/index.d.ts",
63
+ "import": "./dist/features/help/index.mjs",
64
+ "require": "./dist/features/help/index.js"
65
+ },
66
+ "./help/server": {
67
+ "types": "./dist/features/help/server.d.ts",
68
+ "import": "./dist/features/help/server.mjs",
69
+ "require": "./dist/features/help/server.js"
55
70
  }
56
71
  },
57
72
  "scripts": {
@@ -139,6 +154,10 @@
139
154
  "react-markdown": "^10.1.0",
140
155
  "react-resizable-panels": "^4.11.1",
141
156
  "recharts": "^3.8.1",
157
+ "fuse.js": "^7.0.0",
158
+ "next-mdx-remote": "^5.0.0",
159
+ "rehype-autolink-headings": "^7.1.0",
160
+ "rehype-slug": "^6.0.0",
142
161
  "remark-gfm": "^4.0.1",
143
162
  "shadcn": "^4.8.0",
144
163
  "shepherd.js": "^15.2.2",
@@ -2,6 +2,8 @@
2
2
 
3
3
  import { ModuleWithPermissions } from "../permissions/types";
4
4
  import { setBootstrapper } from "../core/registry/bootstrapStore";
5
+ import { _setStaticHelpContent } from "../core/registry/helpStore";
6
+ import type { HelpContentConfig } from "../features/help/interfaces/help-content-config.interface";
5
7
 
6
8
  // Config storage for client-side contexts
7
9
  let _clientConfig: {
@@ -16,6 +18,7 @@ let _clientConfig: {
16
18
  /**
17
19
  * Configure the JSON:API client. This is the main configuration function.
18
20
  * This is typically called during app initialization.
21
+ * @param config.helpContent - Optional help-content config (manifest, brand, redirects). Forwarded to the help feature's globalThis-backed store; not stored on the client config.
19
22
  */
20
23
  export function configureJsonApi(config: {
21
24
  apiUrl: string;
@@ -24,8 +27,13 @@ export function configureJsonApi(config: {
24
27
  bootstrapper?: () => void;
25
28
  additionalHeaders?: Record<string, string>;
26
29
  stripePublishableKey?: string;
30
+ helpContent?: HelpContentConfig;
27
31
  }): void {
28
- _clientConfig = config;
32
+ const { helpContent, ...rest } = config;
33
+ _clientConfig = rest;
34
+ if (helpContent) {
35
+ _setStaticHelpContent(helpContent);
36
+ }
29
37
  // Register and call bootstrapper to register all modules
30
38
  if (config.bootstrapper) {
31
39
  setBootstrapper(config.bootstrapper);
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Centralized help-content store accessible from client and server contexts.
3
+ * Uses globalThis Symbol keys to persist across HMR/Turbopack reloads and to
4
+ * bridge between the client-side `configureJsonApi` (in `client/config.ts`)
5
+ * and server-side consumers (HelpProvider, getHelpContent, serializeHelpArticle).
6
+ *
7
+ * NO external dependencies to avoid circular imports.
8
+ *
9
+ * Pattern mirrors `bootstrapStore.ts`.
10
+ */
11
+
12
+ // Help content config shape is duplicated here as `unknown` to avoid pulling
13
+ // the help feature into this core file. Callers cast on read.
14
+ type HelpReader = (article: { path: string }) => Promise<string>;
15
+
16
+ const HELP_CONTENT_KEY = Symbol.for("nextjs-jsonapi:helpContent");
17
+ const HELP_READER_KEY = Symbol.for("nextjs-jsonapi:helpReader");
18
+
19
+ const globalStore = globalThis as unknown as {
20
+ [HELP_CONTENT_KEY]?: unknown | null;
21
+ [HELP_READER_KEY]?: HelpReader | null;
22
+ };
23
+
24
+ if (globalStore[HELP_CONTENT_KEY] === undefined) {
25
+ globalStore[HELP_CONTENT_KEY] = null;
26
+ }
27
+ if (globalStore[HELP_READER_KEY] === undefined) {
28
+ globalStore[HELP_READER_KEY] = null;
29
+ }
30
+
31
+ export function _setStaticHelpContent(cfg: unknown | null): void {
32
+ globalStore[HELP_CONTENT_KEY] = cfg;
33
+ }
34
+
35
+ export function _getStaticHelpContent<T = unknown>(): T | null {
36
+ return (globalStore[HELP_CONTENT_KEY] as T | null) ?? null;
37
+ }
38
+
39
+ export function setHelpReader(reader: HelpReader): void {
40
+ globalStore[HELP_READER_KEY] = reader;
41
+ }
42
+
43
+ export function _getStaticHelpReader(): HelpReader | null {
44
+ return globalStore[HELP_READER_KEY] ?? null;
45
+ }
@@ -20,7 +20,7 @@ interface AssistantContextValue {
20
20
  sending: boolean;
21
21
  status?: string;
22
22
  failedMessageIds: Set<string>;
23
- sendMessage(content: string): Promise<void>;
23
+ sendMessage(content: string, opts?: { howToMode?: boolean; limitToHowToId?: string }): Promise<void>;
24
24
  retrySend(tempId: string): Promise<void>;
25
25
  selectThread(id: string): Promise<void>;
26
26
  startNew(): void;
@@ -34,10 +34,17 @@ interface Props {
34
34
  children: React.ReactNode;
35
35
  dehydratedAssistant?: JsonApiHydratedDataInterface;
36
36
  dehydratedMessages?: JsonApiHydratedDataInterface[];
37
+ /**
38
+ * When `true` (default), the provider mutates the browser URL on
39
+ * create/selectThread/startNew (e.g. `/assistants/{id}`). Set to `false`
40
+ * when the assistant is hosted inside a sheet / overlay so the user's
41
+ * current route is preserved.
42
+ */
43
+ manageUrl?: boolean;
37
44
  }
38
45
 
39
46
  function stripOptimistic(list: AssistantMessageInterface[]): AssistantMessageInterface[] {
40
- return list.filter((m) => !m.id.startsWith("tmp-"));
47
+ return list.filter((m) => !m.isOptimistic);
41
48
  }
42
49
 
43
50
  function nextPosition(list: AssistantMessageInterface[]): number {
@@ -55,7 +62,7 @@ function withPatchedTitle(source: AssistantInterface, title: string): AssistantI
55
62
  });
56
63
  }
57
64
 
58
- export function AssistantProvider({ children, dehydratedAssistant, dehydratedMessages }: Props) {
65
+ export function AssistantProvider({ children, dehydratedAssistant, dehydratedMessages, manageUrl = true }: Props) {
59
66
  const t = useTranslations();
60
67
  const generateUrl = usePageUrlGenerator();
61
68
 
@@ -73,7 +80,7 @@ export function AssistantProvider({ children, dehydratedAssistant, dehydratedMes
73
80
  const { socket } = useSocketContext();
74
81
 
75
82
  const sendMessage = useCallback(
76
- async (content: string) => {
83
+ async (content: string, opts?: { howToMode?: boolean; limitToHowToId?: string }) => {
77
84
  const trimmed = content.trim();
78
85
  if (!trimmed) return;
79
86
 
@@ -94,18 +101,24 @@ export function AssistantProvider({ children, dehydratedAssistant, dehydratedMes
94
101
 
95
102
  try {
96
103
  if (!assistant) {
97
- const created = await AssistantService.create({ firstMessage: trimmed });
104
+ const created = await AssistantService.create({
105
+ firstMessage: trimmed,
106
+ howToMode: opts?.howToMode,
107
+ limitToHowToId: opts?.limitToHowToId,
108
+ });
98
109
  const msgs = await AssistantMessageService.findByAssistant({ assistantId: created.id });
99
110
  setAssistant(created);
100
111
  setMessages(msgs);
101
112
  setThreads((prev) => [created, ...prev]);
102
- if (typeof window !== "undefined") {
113
+ if (manageUrl && typeof window !== "undefined") {
103
114
  window.history.replaceState(null, "", `/assistants/${created.id}`);
104
115
  }
105
116
  } else {
106
117
  const result = await AssistantService.appendMessage({
107
118
  assistantId: assistant.id,
108
119
  content: trimmed,
120
+ howToMode: opts?.howToMode,
121
+ limitToHowToId: opts?.limitToHowToId,
109
122
  });
110
123
  setMessages((prev) => [...stripOptimistic(prev), ...result]);
111
124
  }
@@ -142,17 +155,20 @@ export function AssistantProvider({ children, dehydratedAssistant, dehydratedMes
142
155
  [messages, sendMessage],
143
156
  );
144
157
 
145
- const selectThread = useCallback(async (id: string) => {
146
- const [target, msgs] = await Promise.all([
147
- AssistantService.findOne({ id }),
148
- AssistantMessageService.findByAssistant({ assistantId: id }),
149
- ]);
150
- setAssistant(target);
151
- setMessages(msgs);
152
- if (typeof window !== "undefined") {
153
- window.history.replaceState(null, "", `/assistants/${id}`);
154
- }
155
- }, []);
158
+ const selectThread = useCallback(
159
+ async (id: string) => {
160
+ const [target, msgs] = await Promise.all([
161
+ AssistantService.findOne({ id }),
162
+ AssistantMessageService.findByAssistant({ assistantId: id }),
163
+ ]);
164
+ setAssistant(target);
165
+ setMessages(msgs);
166
+ if (manageUrl && typeof window !== "undefined") {
167
+ window.history.replaceState(null, "", `/assistants/${id}`);
168
+ }
169
+ },
170
+ [manageUrl],
171
+ );
156
172
 
157
173
  const renameThread = useCallback(async (id: string, title: string) => {
158
174
  await AssistantService.rename({ id, title });
@@ -164,10 +180,10 @@ export function AssistantProvider({ children, dehydratedAssistant, dehydratedMes
164
180
  setAssistant(undefined);
165
181
  setMessages([]);
166
182
  setFailedMessageIds(new Set());
167
- if (typeof window !== "undefined") {
183
+ if (manageUrl && typeof window !== "undefined") {
168
184
  window.history.replaceState(null, "", "/assistants");
169
185
  }
170
- }, []);
186
+ }, [manageUrl]);
171
187
 
172
188
  const deleteThread = useCallback(async (id: string) => {
173
189
  await AssistantService.delete({ id });
@@ -264,7 +264,7 @@ describe("AssistantContext", () => {
264
264
 
265
265
  // Before the server responds, the optimistic user bubble must be visible.
266
266
  expect(result.current.messages.map((m) => m.content)).toContain("follow-up");
267
- expect(result.current.messages.some((m) => m.id.startsWith("tmp-") && m.role === "user")).toBe(true);
267
+ expect(result.current.messages.some((m) => m.isOptimistic && m.role === "user")).toBe(true);
268
268
  expect(result.current.sending).toBe(true);
269
269
 
270
270
  await act(async () => {
@@ -276,7 +276,7 @@ describe("AssistantContext", () => {
276
276
  });
277
277
 
278
278
  // After reconciliation, no tmp-* remains, and server messages are appended.
279
- expect(result.current.messages.some((m) => m.id.startsWith("tmp-"))).toBe(false);
279
+ expect(result.current.messages.some((m) => m.isOptimistic)).toBe(false);
280
280
  expect(result.current.messages.map((m) => m.content)).toEqual(["follow-up", "reply"]);
281
281
  expect(result.current.sending).toBe(false);
282
282
  });
@@ -307,7 +307,7 @@ describe("AssistantContext", () => {
307
307
 
308
308
  // Before the server responds: thread has exactly the optimistic user bubble.
309
309
  expect(result.current.messages).toHaveLength(1);
310
- expect(result.current.messages[0].id.startsWith("tmp-")).toBe(true);
310
+ expect(result.current.messages[0].isOptimistic).toBe(true);
311
311
  expect(result.current.messages[0].content).toBe("first question");
312
312
  expect(result.current.assistant).toBeUndefined();
313
313
  expect(result.current.sending).toBe(true);
@@ -319,11 +319,71 @@ describe("AssistantContext", () => {
319
319
 
320
320
  // After reconciliation: assistant set, URL replaced, server messages only.
321
321
  expect(result.current.assistant?.id).toBe("a-1");
322
- expect(result.current.messages.some((m) => m.id.startsWith("tmp-"))).toBe(false);
322
+ expect(result.current.messages.some((m) => m.isOptimistic)).toBe(false);
323
323
  expect(result.current.messages).toHaveLength(2);
324
324
  expect(replaceState).toHaveBeenCalledWith(null, "", "/assistants/a-1");
325
325
  });
326
326
 
327
+ it("forwards howToMode to AssistantService.create on first send", async () => {
328
+ const replaceState = vi.spyOn(window.history, "replaceState").mockImplementation(() => {});
329
+ const created = buildAssistantStub({ id: "a-1" });
330
+ AssistantService.create = vi.fn().mockResolvedValue(created);
331
+ AssistantMessageService.findByAssistant = vi.fn().mockResolvedValue([]);
332
+
333
+ const { result } = renderHook(() => useAssistantContext(), {
334
+ wrapper: ({ children }) => <AssistantProvider>{children}</AssistantProvider>,
335
+ });
336
+ await act(async () => {
337
+ await result.current.sendMessage("hi", { howToMode: true });
338
+ });
339
+
340
+ expect(AssistantService.create).toHaveBeenCalledWith(
341
+ expect.objectContaining({ firstMessage: "hi", howToMode: true }),
342
+ );
343
+ replaceState.mockRestore();
344
+ });
345
+
346
+ it("forwards howToMode to AssistantService.appendMessage on follow-up send", async () => {
347
+ const existing = buildAssistantDehydrated({ id: "a-2", title: "Existing" });
348
+ AssistantService.appendMessage = vi.fn().mockResolvedValue([]);
349
+
350
+ const { result } = renderHook(() => useAssistantContext(), {
351
+ wrapper: ({ children }) => <AssistantProvider dehydratedAssistant={existing}>{children}</AssistantProvider>,
352
+ });
353
+ await act(async () => {
354
+ await result.current.sendMessage("follow up", { howToMode: true, limitToHowToId: "ht-1" });
355
+ });
356
+
357
+ expect(AssistantService.appendMessage).toHaveBeenCalledWith(
358
+ expect.objectContaining({
359
+ assistantId: "a-2",
360
+ content: "follow up",
361
+ howToMode: true,
362
+ limitToHowToId: "ht-1",
363
+ }),
364
+ );
365
+ });
366
+
367
+ it("calls service without opts when called with content only (regression)", async () => {
368
+ const replaceState = vi.spyOn(window.history, "replaceState").mockImplementation(() => {});
369
+ const created = buildAssistantStub({ id: "a-1" });
370
+ AssistantService.create = vi.fn().mockResolvedValue(created);
371
+ AssistantMessageService.findByAssistant = vi.fn().mockResolvedValue([]);
372
+
373
+ const { result } = renderHook(() => useAssistantContext(), {
374
+ wrapper: ({ children }) => <AssistantProvider>{children}</AssistantProvider>,
375
+ });
376
+ await act(async () => {
377
+ await result.current.sendMessage("hi");
378
+ });
379
+
380
+ expect(AssistantService.create).toHaveBeenCalledWith(expect.objectContaining({ firstMessage: "hi" }));
381
+ const call = (AssistantService.create as ReturnType<typeof vi.fn>).mock.calls[0][0];
382
+ expect(call.howToMode).toBeUndefined();
383
+ expect(call.limitToHowToId).toBeUndefined();
384
+ replaceState.mockRestore();
385
+ });
386
+
327
387
  it("sendMessage failure: optimistic message stays and its id lands in failedMessageIds", async () => {
328
388
  const existing = buildAssistantDehydrated({ id: "a-x", title: "Ex" });
329
389
  AssistantService.appendMessage = vi.fn().mockRejectedValue(new Error("boom"));
@@ -335,7 +395,7 @@ describe("AssistantContext", () => {
335
395
  await result.current.sendMessage("oops").catch(() => {});
336
396
  });
337
397
 
338
- const optimistic = result.current.messages.find((m) => m.id.startsWith("tmp-"));
398
+ const optimistic = result.current.messages.find((m) => m.isOptimistic);
339
399
  expect(optimistic).toBeDefined();
340
400
  expect(optimistic!.content).toBe("oops");
341
401
  expect(result.current.failedMessageIds.has(optimistic!.id)).toBe(true);
@@ -368,7 +428,7 @@ describe("AssistantContext", () => {
368
428
  });
369
429
 
370
430
  expect(result.current.failedMessageIds.has(failedId!)).toBe(false);
371
- expect(result.current.messages.some((m) => m.id.startsWith("tmp-"))).toBe(false);
431
+ expect(result.current.messages.some((m) => m.isOptimistic)).toBe(false);
372
432
  expect(result.current.messages.map((m) => m.content)).toEqual(["retry-me", "ok"]);
373
433
  expect(appendMock).toHaveBeenCalledTimes(2);
374
434
  });
@@ -29,6 +29,8 @@ export class Assistant extends AbstractApiData implements AssistantInterface {
29
29
  attributes: {
30
30
  content: data.firstMessage,
31
31
  ...(data.title !== undefined ? { title: data.title } : {}),
32
+ ...(data.howToMode !== undefined ? { howToMode: data.howToMode } : {}),
33
+ ...(data.limitToHowToId !== undefined ? { limitToHowToId: data.limitToHowToId } : {}),
32
34
  },
33
35
  },
34
36
  included: [],
@@ -3,6 +3,8 @@ import { ApiDataInterface } from "../../../core";
3
3
  export type AssistantInput = {
4
4
  firstMessage: string;
5
5
  title?: string;
6
+ howToMode?: boolean;
7
+ limitToHowToId?: string;
6
8
  };
7
9
 
8
10
  export interface AssistantInterface extends ApiDataInterface {
@@ -1,6 +1,7 @@
1
1
  import { AbstractService, EndpointCreator, HttpMethod, Modules } from "../../../core";
2
- import { AssistantInput, AssistantInterface } from "./AssistantInterface";
2
+ import { AssistantMessage } from "../../assistant-message/data/AssistantMessage";
3
3
  import { AssistantMessageInterface } from "../../assistant-message/data/AssistantMessageInterface";
4
+ import { AssistantInput, AssistantInterface } from "./AssistantInterface";
4
5
 
5
6
  export class AssistantService extends AbstractService {
6
7
  static async findOne(params: { id: string }): Promise<AssistantInterface> {
@@ -33,8 +34,18 @@ export class AssistantService extends AbstractService {
33
34
  /**
34
35
  * Sends a new user message to an existing assistant thread. The agent turn
35
36
  * runs synchronously; the response is a two-element list: [user, assistant].
37
+ *
38
+ * Uses the dedicated AssistantMessage.createAppendMessageJsonApi method to
39
+ * build the JSON:API envelope; this is the architecture-compliant pairing
40
+ * with `overridesJsonApiCreation: true`.
36
41
  */
37
- static async appendMessage(params: { assistantId: string; content: string }): Promise<AssistantMessageInterface[]> {
42
+ static async appendMessage(params: {
43
+ assistantId: string;
44
+ content: string;
45
+ howToMode?: boolean;
46
+ limitToHowToId?: string;
47
+ }): Promise<AssistantMessageInterface[]> {
48
+ const message = new AssistantMessage();
38
49
  return this.callApi<AssistantMessageInterface[]>({
39
50
  type: Modules.AssistantMessage,
40
51
  method: HttpMethod.POST,
@@ -43,12 +54,11 @@ export class AssistantService extends AbstractService {
43
54
  id: params.assistantId,
44
55
  childEndpoint: Modules.AssistantMessage,
45
56
  }).generate(),
46
- input: {
47
- data: {
48
- type: Modules.AssistantMessage.name,
49
- attributes: { content: params.content },
50
- },
51
- },
57
+ input: message.createAppendMessageJsonApi({
58
+ content: params.content,
59
+ howToMode: params.howToMode,
60
+ limitToHowToId: params.limitToHowToId,
61
+ }),
52
62
  overridesJsonApiCreation: true,
53
63
  });
54
64
  }
@@ -55,12 +55,14 @@ export function MessageSourcesPanel({ message, isLatestAssistant, onSelectFollow
55
55
  const suggestionsCount = isLatestAssistant ? message.suggestedQuestions.length : 0;
56
56
 
57
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).
58
+ // Count unique nodeIds across citations. Mirrors what ContentsTab actually
59
+ // renders (one row per cited source), instead of `sources.size` which can
60
+ // include entities the fetch returned but no citation references.
61
61
  const ids = new Set<string>();
62
62
  for (const c of message.citations) {
63
- if (c.nodeId) ids.add(c.nodeId);
63
+ if (!c.nodeId) continue;
64
+ if (sources && !sources.has(c.nodeId)) continue;
65
+ ids.add(c.nodeId);
64
66
  }
65
67
  return ids.size;
66
68
  }, [message.citations, sources]);
@@ -64,7 +64,11 @@ export function ContentsTab({ citations, sources }: Props) {
64
64
  } catch {
65
65
  return null;
66
66
  }
67
- const href = generate({ page: module, id: source.id });
67
+ // Help-content HowTos are public articles, not admin records: route to
68
+ // /help/<mode>/<slug> instead of the module's /administration/howtos
69
+ // page when the entity carries a helpContentSlug.
70
+ const helpContentSlug = (source as any).helpContentSlug as string | undefined;
71
+ const href = helpContentSlug ? `/help/${helpContentSlug}` : generate({ page: module, id: source.id });
68
72
  const name = (source as any).name ?? source.identifier;
69
73
  return (
70
74
  <TableRow key={`${source.type}/${source.id}`}>
@@ -33,7 +33,8 @@ export function ReferencesTab({ references }: Props) {
33
33
  } catch {
34
34
  return null;
35
35
  }
36
- const href = generate({ page: module, id: ref.id });
36
+ const helpContentSlug = (ref as any).helpContentSlug as string | undefined;
37
+ const href = helpContentSlug ? `/help/${helpContentSlug}` : generate({ page: module, id: ref.id });
37
38
  return (
38
39
  <TableRow key={`${ref.type}/${ref.id}`}>
39
40
  <TableCell>