@carlonicora/nextjs-jsonapi 3.12.0 → 3.14.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 (145) hide show
  1. package/dist/{AssistantInterface-DwdBzS9f.d.ts → AssistantInterface-RvZzlcA_.d.ts} +2 -0
  2. package/dist/{AssistantInterface-DMh-zApr.d.mts → AssistantInterface-jL56dyNc.d.mts} +2 -0
  3. package/dist/{AssistantMessageInterface-0zyVingf.d.ts → AssistantMessageInterface-CuaMDs2i.d.ts} +1 -1
  4. package/dist/{AssistantMessageInterface-ddyG114R.d.mts → AssistantMessageInterface-Du3ShThp.d.mts} +1 -1
  5. package/dist/{BlockNoteEditor-CR5KH4AI.mjs → BlockNoteEditor-QG5OWH5K.mjs} +4 -4
  6. package/dist/{BlockNoteEditor-6G27S6P3.js → BlockNoteEditor-WP4K423H.js} +19 -19
  7. package/dist/{BlockNoteEditor-6G27S6P3.js.map → BlockNoteEditor-WP4K423H.js.map} +1 -1
  8. package/dist/{BlockNoteViewer-5MITJ7AD.js → BlockNoteViewer-CWFPYYIU.js} +3 -3
  9. package/dist/{BlockNoteViewer-5MITJ7AD.js.map → BlockNoteViewer-CWFPYYIU.js.map} +1 -1
  10. package/dist/{BlockNoteViewer-NJKFUNXP.mjs → BlockNoteViewer-P2VKEFRN.mjs} +2 -2
  11. package/dist/{ai-connection.module-28f-hux0.d.mts → HandbookSectionInterface-BOxIdBdd.d.mts} +54 -1
  12. package/dist/{ai-connection.module-Bqwh_DnR.d.ts → HandbookSectionInterface-CAXxbDw0.d.ts} +54 -1
  13. package/dist/{auth.interface-B6ZQLGjS.d.ts → auth.interface-BC1jp_ni.d.ts} +1 -1
  14. package/dist/{auth.interface-jYD8Gc4P.d.mts → auth.interface-dR2-6gPM.d.mts} +1 -1
  15. package/dist/billing/index.js +401 -401
  16. package/dist/billing/index.mjs +3 -3
  17. package/dist/{chunk-JEGL7KHK.mjs → chunk-46BRB36R.mjs} +2847 -1803
  18. package/dist/chunk-46BRB36R.mjs.map +1 -0
  19. package/dist/{chunk-L7DZSTFU.js → chunk-IB7DTXWG.js} +846 -129
  20. package/dist/chunk-IB7DTXWG.js.map +1 -0
  21. package/dist/{chunk-OKGGYCO5.js → chunk-OH6GJY4B.js} +2162 -1118
  22. package/dist/chunk-OH6GJY4B.js.map +1 -0
  23. package/dist/{chunk-Q2BREXH3.mjs → chunk-OSFYCSTW.mjs} +724 -7
  24. package/dist/chunk-OSFYCSTW.mjs.map +1 -0
  25. package/dist/{chunk-HVNEWDRF.mjs → chunk-VZKJIAND.mjs} +2 -2
  26. package/dist/{chunk-4ETVCRQD.js → chunk-XH4IURUD.js} +7 -7
  27. package/dist/{chunk-4ETVCRQD.js.map → chunk-XH4IURUD.js.map} +1 -1
  28. package/dist/client/index.d.mts +2 -2
  29. package/dist/client/index.d.ts +2 -2
  30. package/dist/client/index.js +4 -4
  31. package/dist/client/index.mjs +3 -3
  32. package/dist/components/index.d.mts +270 -7
  33. package/dist/components/index.d.ts +270 -7
  34. package/dist/components/index.js +32 -4
  35. package/dist/components/index.js.map +1 -1
  36. package/dist/components/index.mjs +31 -3
  37. package/dist/contexts/index.d.mts +17 -5
  38. package/dist/contexts/index.d.ts +17 -5
  39. package/dist/contexts/index.js +4 -4
  40. package/dist/contexts/index.mjs +3 -3
  41. package/dist/core/index.d.mts +491 -11
  42. package/dist/core/index.d.ts +491 -11
  43. package/dist/core/index.js +38 -2
  44. package/dist/core/index.js.map +1 -1
  45. package/dist/core/index.mjs +37 -1
  46. package/dist/features/help/index.js +37 -37
  47. package/dist/features/help/index.mjs +3 -3
  48. package/dist/features/tokenusage/index.js +101 -101
  49. package/dist/features/tokenusage/index.mjs +3 -3
  50. package/dist/index.d.mts +7 -7
  51. package/dist/index.d.ts +7 -7
  52. package/dist/index.js +39 -3
  53. package/dist/index.js.map +1 -1
  54. package/dist/index.mjs +38 -2
  55. package/dist/{notification.interface-DYF56CtE.d.ts → notification.interface-CjzV_KII.d.ts} +1 -1
  56. package/dist/{notification.interface-BWOCvKRP.d.mts → notification.interface-MnRt1Tjm.d.mts} +1 -1
  57. package/dist/{s3.service-D_dswjgz.d.mts → s3.service-C0Yq2Sx3.d.mts} +2 -2
  58. package/dist/{s3.service-DserU9VK.d.ts → s3.service-CUGarZs3.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/{useSocket-BZG9oX-L.d.mts → useSocket-BcH7l7B8.d.mts} +1 -1
  64. package/dist/{useSocket-CiyTtYFj.d.ts → useSocket-DCZd_eJk.d.ts} +1 -1
  65. package/package.json +2 -1
  66. package/src/components/containers/MermaidDiagram.tsx +117 -0
  67. package/src/components/containers/ReactMarkdownContainer.tsx +98 -15
  68. package/src/components/containers/index.ts +1 -0
  69. package/src/components/index.ts +1 -0
  70. package/src/core/index.ts +5 -0
  71. package/src/core/registry/ModuleRegistry.ts +4 -0
  72. package/src/features/assistant/components/parts/AssistantComposer.tsx +22 -3
  73. package/src/features/assistant/components/parts/AssistantSidebar.tsx +13 -3
  74. package/src/features/assistant/components/parts/AssistantThreadHeader.tsx +10 -3
  75. package/src/features/assistant/contexts/AssistantContext.tsx +30 -3
  76. package/src/features/assistant/contexts/__tests__/AssistantContext.spec.tsx +32 -0
  77. package/src/features/assistant/data/Assistant.ts +2 -0
  78. package/src/features/assistant/data/AssistantInterface.ts +2 -0
  79. package/src/features/assistant/data/AssistantService.ts +4 -0
  80. package/src/features/assistant-message/components/parts/MessageSourcesContainer.tsx +5 -1
  81. package/src/features/assistant-message/data/AssistantMessage.ts +27 -2
  82. package/src/features/content/data/content.interface.ts +1 -1
  83. package/src/features/handbook/HandbookPageModule.ts +41 -0
  84. package/src/features/handbook/HandbookSectionModule.ts +35 -0
  85. package/src/features/handbook/HandbookThreadMessageModule.ts +21 -0
  86. package/src/features/handbook/HandbookThreadModule.ts +29 -0
  87. package/src/features/handbook/components/containers/HandbookAdminContainer.tsx +17 -0
  88. package/src/features/handbook/components/containers/HandbookAskContainer.tsx +142 -0
  89. package/src/features/handbook/components/containers/HandbookAskSheet.tsx +149 -0
  90. package/src/features/handbook/components/containers/HandbookPageContainer.tsx +143 -0
  91. package/src/features/handbook/components/containers/__tests__/HandbookAskContainer.spec.tsx +268 -0
  92. package/src/features/handbook/components/containers/__tests__/HandbookAskSheet.spec.tsx +143 -0
  93. package/src/features/handbook/components/containers/__tests__/HandbookPageContainer.spec.tsx +288 -0
  94. package/src/features/handbook/components/details/HandbookPageDetails.tsx +52 -0
  95. package/src/features/handbook/components/details/HandbookPageNeighbours.tsx +72 -0
  96. package/src/features/handbook/components/details/HandbookPageToc.tsx +165 -0
  97. package/src/features/handbook/components/forms/HandbookSyncButton.tsx +43 -0
  98. package/src/features/handbook/components/forms/__tests__/HandbookSyncButton.spec.tsx +42 -0
  99. package/src/features/handbook/components/index.ts +12 -0
  100. package/src/features/handbook/components/lists/HandbookContents.tsx +196 -0
  101. package/src/features/handbook/components/lists/HandbookSectionGroup.tsx +153 -0
  102. package/src/features/handbook/components/lists/__tests__/HandbookContents.spec.tsx +247 -0
  103. package/src/features/handbook/components/parts/HandbookIndexStatus.tsx +62 -0
  104. package/src/features/handbook/components/parts/HandbookPageNavigator.tsx +143 -0
  105. package/src/features/handbook/contexts/HandbookContext.tsx +109 -0
  106. package/src/features/handbook/contexts/__tests__/HandbookContext.spec.tsx +131 -0
  107. package/src/features/handbook/data/HandbookPage.spec.ts +136 -0
  108. package/src/features/handbook/data/HandbookPage.ts +99 -0
  109. package/src/features/handbook/data/HandbookPageFields.ts +31 -0
  110. package/src/features/handbook/data/HandbookPageInput.ts +8 -0
  111. package/src/features/handbook/data/HandbookPageInterface.ts +29 -0
  112. package/src/features/handbook/data/HandbookPageService.ts +60 -0
  113. package/src/features/handbook/data/HandbookSection.spec.ts +58 -0
  114. package/src/features/handbook/data/HandbookSection.ts +73 -0
  115. package/src/features/handbook/data/HandbookSectionInput.ts +12 -0
  116. package/src/features/handbook/data/HandbookSectionInterface.ts +26 -0
  117. package/src/features/handbook/data/HandbookSectionService.ts +26 -0
  118. package/src/features/handbook/data/HandbookThread.ts +114 -0
  119. package/src/features/handbook/data/HandbookThreadInterface.ts +31 -0
  120. package/src/features/handbook/data/HandbookThreadMessage.ts +144 -0
  121. package/src/features/handbook/data/HandbookThreadMessageInterface.ts +35 -0
  122. package/src/features/handbook/data/HandbookThreadMessageService.ts +30 -0
  123. package/src/features/handbook/data/HandbookThreadService.ts +86 -0
  124. package/src/features/handbook/data/__tests__/HandbookThreadService.spec.ts +112 -0
  125. package/src/features/handbook/data/handbookDisplay.spec.ts +92 -0
  126. package/src/features/handbook/data/handbookDisplay.ts +55 -0
  127. package/src/features/handbook/data/index.ts +16 -0
  128. package/src/features/handbook/hooks/__tests__/useHandbookAsk.spec.ts +198 -0
  129. package/src/features/handbook/hooks/useHandbookAsk.ts +213 -0
  130. package/src/features/handbook/i18n-keys.ts +46 -0
  131. package/src/features/handbook/index.ts +7 -0
  132. package/src/features/how-to/components/forms/HowToEditor.tsx +0 -1
  133. package/src/features/how-to/components/forms/HowToReindexButton.spec.tsx +84 -0
  134. package/src/features/how-to/components/forms/HowToReindexButton.tsx +46 -0
  135. package/src/features/how-to/components/index.ts +1 -0
  136. package/src/features/how-to/components/lists/HowToList.tsx +5 -1
  137. package/src/features/how-to/data/HowTo.spec.ts +61 -1
  138. package/src/features/how-to/data/HowToService.ts +13 -0
  139. package/dist/chunk-JEGL7KHK.mjs.map +0 -1
  140. package/dist/chunk-L7DZSTFU.js.map +0 -1
  141. package/dist/chunk-OKGGYCO5.js.map +0 -1
  142. package/dist/chunk-Q2BREXH3.mjs.map +0 -1
  143. /package/dist/{BlockNoteEditor-CR5KH4AI.mjs.map → BlockNoteEditor-QG5OWH5K.mjs.map} +0 -0
  144. /package/dist/{BlockNoteViewer-NJKFUNXP.mjs.map → BlockNoteViewer-P2VKEFRN.mjs.map} +0 -0
  145. /package/dist/{chunk-HVNEWDRF.mjs.map → chunk-VZKJIAND.mjs.map} +0 -0
@@ -4,8 +4,34 @@ import { ChevronDown, ChevronUp } from "lucide-react";
4
4
  import { useTranslations } from "next-intl";
5
5
  import { useEffect, useRef, useState } from "react";
6
6
  import ReactMarkdown from "react-markdown";
7
+ import rehypeSlug from "rehype-slug";
7
8
  import remarkGfm from "remark-gfm";
8
9
 
10
+ import { cn } from "../../utils/cn";
11
+ import { MermaidDiagram } from "./MermaidDiagram";
12
+
13
+ /**
14
+ * The source of a ```mermaid fence, or `undefined` for any other `pre`.
15
+ *
16
+ * Reads react-markdown's hast node rather than the rendered children: the
17
+ * language lives in the inner `code` element's `language-*` class, and the
18
+ * source is its raw text. Pulling it off the React children would mean
19
+ * unpicking elements that the `code` override has already transformed.
20
+ */
21
+ function mermaidSourceOf(node: unknown): string | undefined {
22
+ const pre = node as { children?: { tagName?: string; properties?: { className?: unknown }; children?: unknown[] }[] };
23
+ const code = pre?.children?.[0];
24
+ if (code?.tagName !== "code") return undefined;
25
+
26
+ const classNames = Array.isArray(code.properties?.className) ? (code.properties.className as string[]) : [];
27
+ if (!classNames.includes("language-mermaid")) return undefined;
28
+
29
+ const text = code.children?.[0] as { type?: string; value?: string } | undefined;
30
+ if (text?.type !== "text" || typeof text.value !== "string") return undefined;
31
+
32
+ return text.value;
33
+ }
34
+
9
35
  type ReactMarkdownContainerProps = {
10
36
  content: string;
11
37
  collapsible?: boolean;
@@ -52,10 +78,34 @@ export function ReactMarkdownContainer({
52
78
  <div ref={contentRef} style={clampStyle} className="transition-all duration-300 ease-in-out">
53
79
  <ReactMarkdown
54
80
  remarkPlugins={[remarkGfm]}
81
+ // `rehype-slug` puts a stable `id` on every heading. Nothing linked
82
+ // to a heading before, so no markdown surface in the package could
83
+ // carry an in-page table of contents; the handbook reader's one
84
+ // reads these ids straight out of the DOM.
85
+ rehypePlugins={[rehypeSlug]}
55
86
  components={{
56
- p: ({ children }) => <p className={size === "small" ? "text-xs" : ""}>{children}</p>,
57
- li: ({ children }) => <li className={size === "small" ? "text-xs" : ""}>{children}</li>,
58
- table: ({ children }) => <table className="w-full table-auto border-collapse border">{children}</table>,
87
+ // `overflow-wrap:anywhere`, not `break-words`: prose here is full
88
+ // of file paths, and there is no space in
89
+ // `packages/nestjs-neo4jsonapi/src/core/health.controller.ts` for
90
+ // `break-words` to break at. Without it one path widens the page
91
+ // and the whole layout scrolls sideways.
92
+ p: ({ children }) => (
93
+ <p className={cn("[overflow-wrap:anywhere]", size === "small" && "text-xs")}>{children}</p>
94
+ ),
95
+ li: ({ children }) => (
96
+ <li className={cn("[overflow-wrap:anywhere]", size === "small" && "text-xs")}>{children}</li>
97
+ ),
98
+ // Wrapped in its own scroller for the same reason as `pre`: a
99
+ // three-column table of file paths is wider than any reading
100
+ // column, and unwrapped it drags the page sideways with it.
101
+ table: ({ children }) => (
102
+ <div className="overflow-x-auto">
103
+ {/* `min-w-max` so columns keep their natural width and the
104
+ wrapper scrolls, rather than the table squeezing until a
105
+ path breaks mid-word inside a cell. */}
106
+ <table className="w-full min-w-max table-auto border-collapse border">{children}</table>
107
+ </div>
108
+ ),
59
109
  th: ({ children }) => (
60
110
  <th className={`border px-4 py-2 text-start ${size === "small" ? "px-2 py-1 text-xs" : ""}`}>
61
111
  {children}
@@ -73,37 +123,70 @@ export function ReactMarkdownContainer({
73
123
  // left-to-right in every locale, so both the fenced block and the
74
124
  // inline span are deliberate LTR islands. `node` is react-markdown's
75
125
  // hast node — it must not reach the DOM element.
126
+ // A ```mermaid fence is a DIAGRAM, not code. It is caught here on
127
+ // the `pre` rather than on the `code` inside it, because the
128
+ // diagram replaces the whole block — catching it a level down
129
+ // would leave it wrapped in the `pre` chrome it is not.
130
+ //
131
+ // A fenced block otherwise scrolls on its own rather than
132
+ // widening the page: a long command line is unbreakable, and
133
+ // without this the whole layout is pushed sideways.
76
134
  // rtl-ok: deliberate LTR island (code)
77
- pre: ({ children, node: _node, ...props }) => (
78
- <pre dir="ltr" {...props}>
79
- {children}
80
- </pre>
81
- ),
135
+ pre: ({ children, node, ...props }) => {
136
+ const diagram = mermaidSourceOf(node);
137
+ if (diagram !== undefined) return <MermaidDiagram chart={diagram} />;
138
+
139
+ return (
140
+ <pre dir="ltr" {...props} className="overflow-x-auto">
141
+ {children}
142
+ </pre>
143
+ );
144
+ },
145
+ // No wrap rule HERE. The same component renders inline spans, the
146
+ // contents of fenced blocks and the contents of table cells; the
147
+ // last two live in their own scrollers, and breaking a path
148
+ // mid-word inside them is worse than scrolling to it. The rule
149
+ // that catches inline code sits on `p` and `li`, which is where
150
+ // inline code actually lives.
82
151
  // rtl-ok: deliberate LTR island (code)
83
152
  code: ({ children, node: _node, ...props }) => (
84
153
  <code dir="ltr" {...props}>
85
154
  {children}
86
155
  </code>
87
156
  ),
88
- h1: ({ children }) => (
89
- <h1 className={size === "small" ? "my-1 mt-2 text-sm font-bold" : "my-2 mt-4 text-3xl font-semibold"}>
157
+ // The heading overrides spread the node's props, exactly as `pre`
158
+ // and `code` already do: dropping them would throw away the `id`
159
+ // rehype-slug just set and leave every heading unlinkable again.
160
+ // `node` is react-markdown's hast node — it must not reach the DOM.
161
+ h1: ({ children, node: _node, ...props }) => (
162
+ <h1
163
+ {...props}
164
+ className={size === "small" ? "my-1 mt-2 text-sm font-bold" : "my-2 mt-4 text-3xl font-semibold"}
165
+ >
90
166
  {children}
91
167
  </h1>
92
168
  ),
93
- h2: ({ children }) => (
169
+ h2: ({ children, node: _node, ...props }) => (
94
170
  <h2
171
+ {...props}
95
172
  className={size === "small" ? "my-1 mt-2 text-sm font-semibold" : "my-2 mt-4 text-2xl font-semibold"}
96
173
  >
97
174
  {children}
98
175
  </h2>
99
176
  ),
100
- h3: ({ children }) => (
101
- <h3 className={size === "small" ? "my-1 mt-2 text-sm font-medium" : "my-2 mt-4 text-xl font-semibold"}>
177
+ h3: ({ children, node: _node, ...props }) => (
178
+ <h3
179
+ {...props}
180
+ className={size === "small" ? "my-1 mt-2 text-sm font-medium" : "my-2 mt-4 text-xl font-semibold"}
181
+ >
102
182
  {children}
103
183
  </h3>
104
184
  ),
105
- h4: ({ children }) => (
106
- <h4 className={size === "small" ? "my-1 mt-2 text-sm font-medium" : "my-2 mt-4 text-lg font-semibold"}>
185
+ h4: ({ children, node: _node, ...props }) => (
186
+ <h4
187
+ {...props}
188
+ className={size === "small" ? "my-1 mt-2 text-sm font-medium" : "my-2 mt-4 text-lg font-semibold"}
189
+ >
107
190
  {children}
108
191
  </h4>
109
192
  ),
@@ -1,5 +1,6 @@
1
1
  export * from "./PageContainer";
2
2
  export * from "./partitionTabs";
3
+ export * from "./MermaidDiagram";
3
4
  export * from "./ReactMarkdownContainer";
4
5
  export * from "./RoundPageContainer";
5
6
  export * from "./RoundPageContainerTitle";
@@ -26,6 +26,7 @@ export * from "../features/ai-connection";
26
26
  export * from "../features/company/components";
27
27
  export * from "../features/content/components";
28
28
  export * from "../features/how-to/components";
29
+ export * from "../features/handbook/components";
29
30
  export * from "../features/assistant/components";
30
31
  export * from "../features/assistant-message/components";
31
32
  export * from "../features/assistant-action/components";
package/src/core/index.ts CHANGED
@@ -67,6 +67,11 @@ export * from "../features/content/content.module";
67
67
  export * from "../features/content/data";
68
68
  export * from "../features/how-to/HowToModule";
69
69
  export * from "../features/how-to/data";
70
+ export * from "../features/handbook/HandbookPageModule";
71
+ export * from "../features/handbook/HandbookSectionModule";
72
+ export * from "../features/handbook/HandbookThreadMessageModule";
73
+ export * from "../features/handbook/HandbookThreadModule";
74
+ export * from "../features/handbook/data";
70
75
  export * from "../features/assistant/AssistantModule";
71
76
  export * from "../features/assistant/data";
72
77
  export * from "../features/assistant-message/AssistantMessageModule";
@@ -16,6 +16,10 @@ export interface FoundationModuleDefinitions {
16
16
  Module: ModuleWithPermissions;
17
17
  Content: ModuleWithPermissions;
18
18
  HowTo: ModuleWithPermissions;
19
+ HandbookPage: ModuleWithPermissions;
20
+ HandbookSection: ModuleWithPermissions;
21
+ HandbookThread: ModuleWithPermissions;
22
+ HandbookThreadMessage: ModuleWithPermissions;
19
23
  Assistant: ModuleWithPermissions;
20
24
  AssistantMessage: ModuleWithPermissions;
21
25
  AssistantAction: ModuleWithPermissions;
@@ -10,9 +10,28 @@ interface Props {
10
10
  disabled?: boolean;
11
11
  value?: string;
12
12
  onValueChange?: (v: string) => void;
13
+ /**
14
+ * Textarea placeholder. Defaults to the assistant's own copy ("ask me about a
15
+ * matter, a client, a document…"), which is wrong on any surface that is not
16
+ * the law-firm assistant.
17
+ */
18
+ placeholder?: string;
19
+ /**
20
+ * Label of the send button. Defaults to `ui.buttons.save`, which reads
21
+ * "Salva" — correct nowhere in a chat, kept only so existing callers do not
22
+ * move.
23
+ */
24
+ sendLabel?: string;
13
25
  }
14
26
 
15
- export function AssistantComposer({ onSend, disabled, value: controlled, onValueChange }: Props) {
27
+ export function AssistantComposer({
28
+ onSend,
29
+ disabled,
30
+ value: controlled,
31
+ onValueChange,
32
+ placeholder,
33
+ sendLabel,
34
+ }: Props) {
16
35
  const t = useTranslations();
17
36
  const [internal, setInternal] = useState("");
18
37
  const value = controlled ?? internal;
@@ -41,13 +60,13 @@ export function AssistantComposer({ onSend, disabled, value: controlled, onValue
41
60
  value={value}
42
61
  onChange={(e) => setValue(e.target.value)}
43
62
  onKeyDown={onKeyDown}
44
- placeholder={t("features.assistant.composer_placeholder")}
63
+ placeholder={placeholder ?? t("features.assistant.composer_placeholder")}
45
64
  disabled={disabled}
46
65
  rows={2}
47
66
  className="min-h-[48px] resize-none border-0 bg-transparent focus-visible:ring-0"
48
67
  />
49
68
  <Button onClick={submit} disabled={!canSend} size="sm" className="h-8">
50
- <ArrowUp className="me-1 h-4 w-4" /> {t("features.assistant.send")}
69
+ <ArrowUp className="me-1 h-4 w-4" /> {sendLabel ?? t("features.assistant.send")}
51
70
  </Button>
52
71
  </div>
53
72
  <div className="text-muted-foreground text-end text-xs">{t("features.assistant.keyboard_hint")}</div>
@@ -12,9 +12,17 @@ interface Props {
12
12
  activeId?: string;
13
13
  onSelect: (id: string) => void;
14
14
  onNew: () => void;
15
+ /**
16
+ * Label of the "start a new thread" button. Defaults to the assistant's own
17
+ * copy; a surface that is not the assistant (the handbook chat) passes its
18
+ * own so the control does not read "New assistant" on a handbook page.
19
+ */
20
+ newLabel?: string;
21
+ /** Shown when there are no threads. Same rationale as `newLabel`. */
22
+ emptyLabel?: string;
15
23
  }
16
24
 
17
- export function AssistantSidebar({ threads, activeId, onSelect, onNew }: Props) {
25
+ export function AssistantSidebar({ threads, activeId, onSelect, onNew, newLabel, emptyLabel }: Props) {
18
26
  const t = useTranslations();
19
27
  const groups = groupThreadsByBucket(threads);
20
28
 
@@ -44,12 +52,14 @@ export function AssistantSidebar({ threads, activeId, onSelect, onNew }: Props)
44
52
  <aside className="bg-muted/30 flex w-64 flex-col border-e">
45
53
  <div className="border-b p-3">
46
54
  <Button onClick={onNew} className="w-full" size="sm">
47
- <Plus className="me-1 h-4 w-4" /> {t("features.assistant.new")}
55
+ <Plus className="me-1 h-4 w-4" /> {newLabel ?? t("features.assistant.new")}
48
56
  </Button>
49
57
  </div>
50
58
  <div className="flex-1 overflow-y-auto p-2">
51
59
  {threads.length === 0 ? (
52
- <div className="text-muted-foreground mt-6 text-center text-xs">{t("features.assistant.empty_sidebar")}</div>
60
+ <div className="text-muted-foreground mt-6 text-center text-xs">
61
+ {emptyLabel ?? t("features.assistant.empty_sidebar")}
62
+ </div>
53
63
  ) : (
54
64
  <>
55
65
  {renderSection(t("features.assistant.bucket_today"), groups.today)}
@@ -21,9 +21,16 @@ interface Props {
21
21
  assistant: AssistantInterface;
22
22
  onRename: (title: string) => Promise<void>;
23
23
  onDelete: () => Promise<void>;
24
+ /**
25
+ * Placeholder of the rename field. Defaults to the assistant's own copy
26
+ * ("name this assistant"), which misnames the thread on any other surface.
27
+ */
28
+ renamePlaceholder?: string;
29
+ /** Body of the delete confirmation. Same rationale as `renamePlaceholder`. */
30
+ deleteConfirmLabel?: string;
24
31
  }
25
32
 
26
- export function AssistantThreadHeader({ assistant, onRename, onDelete }: Props) {
33
+ export function AssistantThreadHeader({ assistant, onRename, onDelete, renamePlaceholder, deleteConfirmLabel }: Props) {
27
34
  const t = useTranslations();
28
35
  const [renameValue, setRenameValue] = useState(assistant.title);
29
36
  const [renameOpen, setRenameOpen] = useState(false);
@@ -56,7 +63,7 @@ export function AssistantThreadHeader({ assistant, onRename, onDelete }: Props)
56
63
  <Input
57
64
  value={renameValue}
58
65
  onChange={(e) => setRenameValue(e.target.value)}
59
- placeholder={t("features.assistant.rename_placeholder")}
66
+ placeholder={renamePlaceholder ?? t("features.assistant.rename_placeholder")}
60
67
  />
61
68
  <Button onClick={handleRename} size="sm">
62
69
  {t("ui.buttons.save")}
@@ -73,7 +80,7 @@ export function AssistantThreadHeader({ assistant, onRename, onDelete }: Props)
73
80
  />
74
81
  <DialogContent>
75
82
  <DialogHeader>
76
- <DialogTitle>{t("features.assistant.delete_confirm")}</DialogTitle>
83
+ <DialogTitle>{deleteConfirmLabel ?? t("features.assistant.delete_confirm")}</DialogTitle>
77
84
  </DialogHeader>
78
85
  <DialogFooter>
79
86
  <Button variant="outline" onClick={() => setDeleteOpen(false)}>
@@ -22,7 +22,13 @@ interface AssistantContextValue {
22
22
  failedMessageIds: Set<string>;
23
23
  sendMessage(
24
24
  content: string,
25
- opts?: { howToMode?: boolean; limitToHowToId?: string; contentBlocks?: unknown[] },
25
+ opts?: {
26
+ howToMode?: boolean;
27
+ limitToHowToId?: string;
28
+ handbookMode?: boolean;
29
+ limitToHandbookPageId?: string;
30
+ contentBlocks?: unknown[];
31
+ },
26
32
  ): Promise<void>;
27
33
  retrySend(tempId: string): Promise<void>;
28
34
  selectThread(id: string): Promise<void>;
@@ -55,6 +61,13 @@ interface Props {
55
61
  * it, so a scoped surface never shows or creates cross-scope threads.
56
62
  */
57
63
  scope?: { type: string; id: string };
64
+ /**
65
+ * Confines every turn of this provider to one retrieval mode. The handbook
66
+ * surface sets `{ handbookMode: true }` so the flag does not have to be
67
+ * threaded through the composer on each send — the same reason `scope`
68
+ * exists on this provider.
69
+ */
70
+ retrievalMode?: { handbookMode?: boolean; limitToHandbookPageId?: string };
58
71
  /**
59
72
  * Builds the browser URL for a thread. Defaults to `/assistants/{id}` (or
60
73
  * `/assistants` when no thread is active), which is only correct for the
@@ -100,6 +113,7 @@ export function AssistantProvider({
100
113
  titleOverride,
101
114
  breadcrumbsOverride,
102
115
  scope,
116
+ retrievalMode,
103
117
  threadUrl,
104
118
  }: Props) {
105
119
  const t = useTranslations();
@@ -130,7 +144,16 @@ export function AssistantProvider({
130
144
  const { socket } = useSocketContext();
131
145
 
132
146
  const sendMessage = useCallback(
133
- async (content: string, opts?: { howToMode?: boolean; limitToHowToId?: string; contentBlocks?: unknown[] }) => {
147
+ async (
148
+ content: string,
149
+ opts?: {
150
+ howToMode?: boolean;
151
+ limitToHowToId?: string;
152
+ handbookMode?: boolean;
153
+ limitToHandbookPageId?: string;
154
+ contentBlocks?: unknown[];
155
+ },
156
+ ) => {
134
157
  const trimmed = content.trim();
135
158
  if (!trimmed) return;
136
159
 
@@ -155,6 +178,8 @@ export function AssistantProvider({
155
178
  firstMessage: trimmed,
156
179
  howToMode: opts?.howToMode,
157
180
  limitToHowToId: opts?.limitToHowToId,
181
+ handbookMode: opts?.handbookMode ?? retrievalMode?.handbookMode,
182
+ limitToHandbookPageId: opts?.limitToHandbookPageId ?? retrievalMode?.limitToHandbookPageId,
158
183
  contentBlocks: opts?.contentBlocks,
159
184
  boundContent: scope,
160
185
  };
@@ -180,6 +205,8 @@ export function AssistantProvider({
180
205
  content: trimmed,
181
206
  howToMode: opts?.howToMode,
182
207
  limitToHowToId: opts?.limitToHowToId,
208
+ handbookMode: opts?.handbookMode ?? retrievalMode?.handbookMode,
209
+ limitToHandbookPageId: opts?.limitToHandbookPageId ?? retrievalMode?.limitToHandbookPageId,
183
210
  contentBlocks: opts?.contentBlocks,
184
211
  });
185
212
  setMessages((prev) => [...stripOptimistic(prev), ...result]);
@@ -196,7 +223,7 @@ export function AssistantProvider({
196
223
  setStatus(undefined);
197
224
  }
198
225
  },
199
- [assistant, messages, socket, operatorMode, scope, manageUrl, resolveThreadUrl],
226
+ [assistant, messages, socket, operatorMode, scope, retrievalMode, manageUrl, resolveThreadUrl],
200
227
  );
201
228
 
202
229
  const appendResolvedMessage = useCallback((message: AssistantMessageInterface) => {
@@ -455,6 +455,38 @@ describe("AssistantContext", () => {
455
455
  );
456
456
  });
457
457
 
458
+ it("forwards handbookMode passed on the send", async () => {
459
+ const existing = buildAssistantDehydrated({ id: "a-3", title: "Existing" });
460
+ AssistantService.appendMessage = vi.fn().mockResolvedValue([]);
461
+
462
+ const { result } = renderHook(() => useAssistantContext(), {
463
+ wrapper: ({ children }) => <AssistantProvider dehydratedAssistant={existing}>{children}</AssistantProvider>,
464
+ });
465
+ await act(async () => {
466
+ await result.current.sendMessage("Come funziona il modulo chunk?", { handbookMode: true });
467
+ });
468
+
469
+ expect(AssistantService.appendMessage).toHaveBeenCalledWith(expect.objectContaining({ handbookMode: true }));
470
+ });
471
+
472
+ it("forwards the provider's retrievalMode when the send passes nothing", async () => {
473
+ const existing = buildAssistantDehydrated({ id: "a-4", title: "Existing" });
474
+ AssistantService.appendMessage = vi.fn().mockResolvedValue([]);
475
+
476
+ const { result } = renderHook(() => useAssistantContext(), {
477
+ wrapper: ({ children }) => (
478
+ <AssistantProvider dehydratedAssistant={existing} retrievalMode={{ handbookMode: true }}>
479
+ {children}
480
+ </AssistantProvider>
481
+ ),
482
+ });
483
+ await act(async () => {
484
+ await result.current.sendMessage("Come funziona il modulo chunk?");
485
+ });
486
+
487
+ expect(AssistantService.appendMessage).toHaveBeenCalledWith(expect.objectContaining({ handbookMode: true }));
488
+ });
489
+
458
490
  it("calls service without opts when called with content only (regression)", async () => {
459
491
  const replaceState = vi.spyOn(window.history, "replaceState").mockImplementation(() => {});
460
492
  const created = buildAssistantStub({ id: "a-1" });
@@ -58,6 +58,8 @@ export class Assistant extends AbstractApiData implements AssistantInterface {
58
58
  ...(data.title !== undefined ? { title: data.title } : {}),
59
59
  ...(data.howToMode !== undefined ? { howToMode: data.howToMode } : {}),
60
60
  ...(data.limitToHowToId !== undefined ? { limitToHowToId: data.limitToHowToId } : {}),
61
+ ...(data.handbookMode !== undefined ? { handbookMode: data.handbookMode } : {}),
62
+ ...(data.limitToHandbookPageId !== undefined ? { limitToHandbookPageId: data.limitToHandbookPageId } : {}),
61
63
  },
62
64
  ...(data.boundContent !== undefined
63
65
  ? {
@@ -5,6 +5,8 @@ export type AssistantInput = {
5
5
  title?: string;
6
6
  howToMode?: boolean;
7
7
  limitToHowToId?: string;
8
+ handbookMode?: boolean;
9
+ limitToHandbookPageId?: string;
8
10
  /**
9
11
  * BlockNote document behind `firstMessage`. Sent alongside the flattened
10
12
  * text so the server can re-derive the content and read `@`-mentions from
@@ -67,6 +67,8 @@ export class AssistantService extends AbstractService {
67
67
  content: string;
68
68
  howToMode?: boolean;
69
69
  limitToHowToId?: string;
70
+ handbookMode?: boolean;
71
+ limitToHandbookPageId?: string;
70
72
  /** BlockNote document. Serialised by the model into `content`; never its own attribute. */
71
73
  contentBlocks?: unknown[];
72
74
  }): Promise<AssistantMessageInterface[]> {
@@ -83,6 +85,8 @@ export class AssistantService extends AbstractService {
83
85
  content: params.content,
84
86
  howToMode: params.howToMode,
85
87
  limitToHowToId: params.limitToHowToId,
88
+ handbookMode: params.handbookMode,
89
+ limitToHandbookPageId: params.limitToHandbookPageId,
86
90
  contentBlocks: params.contentBlocks,
87
91
  }),
88
92
  overridesJsonApiCreation: true,
@@ -87,7 +87,11 @@ export function MessageSourcesContainer({ message, isLatestAssistant, onSelectFo
87
87
 
88
88
  useEffect(() => {
89
89
  if (groups.size === 0) {
90
- setResolved([]);
90
+ // Functional update, and a no-op when it is already empty. A bare
91
+ // `setResolved([])` sets a NEW array on every run, so a caller whose
92
+ // `message.citations` identity changes per render loops here until React
93
+ // gives up with "Maximum update depth exceeded".
94
+ setResolved((current) => (current.length === 0 ? current : []));
91
95
  return;
92
96
  }
93
97
  let cancelled = false;
@@ -129,6 +129,8 @@ export class AssistantMessage extends AbstractApiData implements AssistantMessag
129
129
  content: string;
130
130
  howToMode?: boolean;
131
131
  limitToHowToId?: string;
132
+ handbookMode?: boolean;
133
+ limitToHandbookPageId?: string;
132
134
  contentBlocks?: unknown[];
133
135
  }) {
134
136
  return {
@@ -142,18 +144,41 @@ export class AssistantMessage extends AbstractApiData implements AssistantMessag
142
144
  content: params.contentBlocks !== undefined ? JSON.stringify(params.contentBlocks) : params.content,
143
145
  ...(params.howToMode !== undefined ? { howToMode: params.howToMode } : {}),
144
146
  ...(params.limitToHowToId !== undefined ? { limitToHowToId: params.limitToHowToId } : {}),
147
+ ...(params.handbookMode !== undefined ? { handbookMode: params.handbookMode } : {}),
148
+ ...(params.limitToHandbookPageId !== undefined
149
+ ? { limitToHandbookPageId: params.limitToHandbookPageId }
150
+ : {}),
145
151
  },
146
152
  },
147
153
  };
148
154
  }
149
155
 
150
156
  static buildOptimistic(params: { content: string; position: number; assistantId?: string }): AssistantMessage {
157
+ return AssistantMessage.buildLocal({ ...params, role: "user", isOptimistic: true });
158
+ }
159
+
160
+ /**
161
+ * A message that exists only on the client, for a surface that renders the
162
+ * chat presentation without persisting a thread.
163
+ *
164
+ * `buildOptimistic` is the user-turn case and delegates here. The handbook
165
+ * ask surface is the other caller: it has no Assistant and no stored
166
+ * messages, but it renders through MessageList/MessageItem like every other
167
+ * chat in the product, so it needs both roles.
168
+ */
169
+ static buildLocal(params: {
170
+ role: AssistantMessageRole;
171
+ content: string;
172
+ position: number;
173
+ assistantId?: string;
174
+ isOptimistic?: boolean;
175
+ }): AssistantMessage {
151
176
  const msg = new AssistantMessage();
152
177
  const jsonApi: Record<string, unknown> = {
153
178
  id: uuidv4(),
154
179
  type: Modules.AssistantMessage.name,
155
180
  attributes: {
156
- role: "user",
181
+ role: params.role,
157
182
  content: params.content,
158
183
  position: params.position,
159
184
  },
@@ -164,7 +189,7 @@ export class AssistantMessage extends AbstractApiData implements AssistantMessag
164
189
  };
165
190
  }
166
191
  msg.rehydrate({ jsonApi: jsonApi as any, included: [] });
167
- msg._isOptimistic = true;
192
+ msg._isOptimistic = params.isOptimistic ?? false;
168
193
  return msg;
169
194
  }
170
195
  }
@@ -5,7 +5,7 @@ export type ContentInput = {
5
5
  id: string;
6
6
  name?: string;
7
7
 
8
- authorId: string;
8
+ authorId?: string;
9
9
  editorIds?: string[];
10
10
  };
11
11
 
@@ -0,0 +1,41 @@
1
+ import { BookOpenIcon } from "lucide-react";
2
+ import { createJsonApiInclusion } from "../../core";
3
+ import { ModuleFactory } from "../../permissions";
4
+ import { HandbookPage } from "./data/HandbookPage";
5
+
6
+ export const HandbookPageModule = (factory: ModuleFactory) =>
7
+ factory({
8
+ moduleId: "b1f1c2d6-6a0e-4e93-9b6d-3f5f4a2c81e7",
9
+ pageUrl: "/administration/handbook",
10
+ name: "handbookpages",
11
+ model: HandbookPage,
12
+ icon: BookOpenIcon,
13
+ inclusions: {
14
+ lists: {
15
+ // `content` is deliberately omitted: the list renders titles and
16
+ // status, and a thousand pages of markdown do not belong in a list
17
+ // payload. `section`, `order` and `summary` are the index fields the
18
+ // contents page groups, orders and describes pages by, so they are the
19
+ // one part of the payload the list surface cannot do without.
20
+ //
21
+ // `displayTitle` and `displaySummary` are the translated halves of the
22
+ // two strings the contents page prints, and a sparse fieldset that
23
+ // omitted them would leave every row in English. `displayContent` is
24
+ // NOT here for the same reason `content` is not: it is the whole
25
+ // markdown body, and it belongs to the detail read alone.
26
+ fields: [
27
+ createJsonApiInclusion("handbookpages", [
28
+ "path",
29
+ "title",
30
+ "displayTitle",
31
+ "section",
32
+ "order",
33
+ "summary",
34
+ "displaySummary",
35
+ "wordCount",
36
+ "aiStatus",
37
+ ]),
38
+ ],
39
+ },
40
+ },
41
+ });
@@ -0,0 +1,35 @@
1
+ import { LibraryIcon } from "lucide-react";
2
+ import { createJsonApiInclusion } from "../../core";
3
+ import { ModuleFactory } from "../../permissions";
4
+ import { HandbookSection } from "./data/HandbookSection";
5
+
6
+ /**
7
+ * One top-level directory of the documentation tree. Like the thread message,
8
+ * it has no `pageUrl` — a section has no page of its own; it is a heading on
9
+ * the contents surface and nothing more.
10
+ */
11
+ export const HandbookSectionModule = (factory: ModuleFactory) =>
12
+ factory({
13
+ moduleId: "6b0a2f74-1c53-4a0e-9c1d-2e7b4a9d6f30",
14
+ name: "handbooksections",
15
+ model: HandbookSection,
16
+ icon: LibraryIcon,
17
+ inclusions: {
18
+ lists: {
19
+ // `HandbookSectionService.findMany` is the ONLY read of this resource and
20
+ // it limits to these fields, so the translated halves have to be here:
21
+ // without them every section heading on the contents page and in the
22
+ // reader's navigator would stay English however good the translation is.
23
+ fields: [
24
+ createJsonApiInclusion("handbooksections", [
25
+ "key",
26
+ "title",
27
+ "displayTitle",
28
+ "summary",
29
+ "displaySummary",
30
+ "order",
31
+ ]),
32
+ ],
33
+ },
34
+ },
35
+ });