@carlonicora/nextjs-jsonapi 3.11.0 → 3.13.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 (156) 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-XOVNTXDY.js → BlockNoteEditor-CFVSHS46.js} +19 -19
  6. package/dist/{BlockNoteEditor-XOVNTXDY.js.map → BlockNoteEditor-CFVSHS46.js.map} +1 -1
  7. package/dist/{BlockNoteEditor-NBHDEZ7W.mjs → BlockNoteEditor-SERY337S.mjs} +4 -4
  8. package/dist/{BlockNoteViewer-C2UUOBAF.js → BlockNoteViewer-CWFPYYIU.js} +3 -3
  9. package/dist/{BlockNoteViewer-C2UUOBAF.js.map → BlockNoteViewer-CWFPYYIU.js.map} +1 -1
  10. package/dist/{BlockNoteViewer-TYLVTIUD.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.d.mts +27 -0
  16. package/dist/billing/index.d.ts +27 -0
  17. package/dist/billing/index.js +697 -450
  18. package/dist/billing/index.js.map +1 -1
  19. package/dist/billing/index.mjs +282 -35
  20. package/dist/billing/index.mjs.map +1 -1
  21. package/dist/{chunk-6J2SQZ3G.js → chunk-IB7DTXWG.js} +847 -129
  22. package/dist/chunk-IB7DTXWG.js.map +1 -0
  23. package/dist/{chunk-YN4LOVL4.js → chunk-LVE3BJ6K.js} +2106 -1109
  24. package/dist/chunk-LVE3BJ6K.js.map +1 -0
  25. package/dist/{chunk-M5QAHSCY.mjs → chunk-OSFYCSTW.mjs} +725 -7
  26. package/dist/chunk-OSFYCSTW.mjs.map +1 -0
  27. package/dist/{chunk-XNGI4HDZ.mjs → chunk-UQXE3SME.mjs} +2100 -1103
  28. package/dist/chunk-UQXE3SME.mjs.map +1 -0
  29. package/dist/{chunk-IMSIVIIC.mjs → chunk-VZKJIAND.mjs} +2 -2
  30. package/dist/{chunk-V6CGCARD.js → chunk-XH4IURUD.js} +7 -7
  31. package/dist/{chunk-V6CGCARD.js.map → chunk-XH4IURUD.js.map} +1 -1
  32. package/dist/client/index.d.mts +2 -2
  33. package/dist/client/index.d.ts +2 -2
  34. package/dist/client/index.js +4 -4
  35. package/dist/client/index.mjs +3 -3
  36. package/dist/components/index.d.mts +258 -7
  37. package/dist/components/index.d.ts +258 -7
  38. package/dist/components/index.js +30 -4
  39. package/dist/components/index.js.map +1 -1
  40. package/dist/components/index.mjs +29 -3
  41. package/dist/contexts/index.d.mts +17 -5
  42. package/dist/contexts/index.d.ts +17 -5
  43. package/dist/contexts/index.js +4 -4
  44. package/dist/contexts/index.mjs +3 -3
  45. package/dist/core/index.d.mts +493 -12
  46. package/dist/core/index.d.ts +493 -12
  47. package/dist/core/index.js +38 -2
  48. package/dist/core/index.js.map +1 -1
  49. package/dist/core/index.mjs +37 -1
  50. package/dist/features/help/index.js +37 -37
  51. package/dist/features/help/index.mjs +3 -3
  52. package/dist/features/tokenusage/index.js +101 -101
  53. package/dist/features/tokenusage/index.mjs +3 -3
  54. package/dist/index.d.mts +7 -7
  55. package/dist/index.d.ts +7 -7
  56. package/dist/index.js +39 -3
  57. package/dist/index.js.map +1 -1
  58. package/dist/index.mjs +38 -2
  59. package/dist/{notification.interface-DYF56CtE.d.ts → notification.interface-CjzV_KII.d.ts} +1 -1
  60. package/dist/{notification.interface-BWOCvKRP.d.mts → notification.interface-MnRt1Tjm.d.mts} +1 -1
  61. package/dist/{s3.service-D_dswjgz.d.mts → s3.service-C0Yq2Sx3.d.mts} +2 -2
  62. package/dist/{s3.service-DserU9VK.d.ts → s3.service-CUGarZs3.d.ts} +2 -2
  63. package/dist/server/index.d.mts +3 -3
  64. package/dist/server/index.d.ts +3 -3
  65. package/dist/server/index.js +3 -3
  66. package/dist/server/index.mjs +1 -1
  67. package/dist/{useSocket-BZG9oX-L.d.mts → useSocket-BcH7l7B8.d.mts} +1 -1
  68. package/dist/{useSocket-CiyTtYFj.d.ts → useSocket-DCZd_eJk.d.ts} +1 -1
  69. package/package.json +1 -1
  70. package/src/components/containers/ReactMarkdownContainer.tsx +61 -11
  71. package/src/components/index.ts +1 -0
  72. package/src/core/index.ts +5 -0
  73. package/src/core/registry/ModuleRegistry.ts +4 -0
  74. package/src/features/assistant/components/parts/AssistantComposer.tsx +22 -3
  75. package/src/features/assistant/components/parts/AssistantSidebar.tsx +13 -3
  76. package/src/features/assistant/components/parts/AssistantThreadHeader.tsx +10 -3
  77. package/src/features/assistant/contexts/AssistantContext.tsx +30 -3
  78. package/src/features/assistant/contexts/__tests__/AssistantContext.spec.tsx +32 -0
  79. package/src/features/assistant/data/Assistant.ts +2 -0
  80. package/src/features/assistant/data/AssistantInterface.ts +2 -0
  81. package/src/features/assistant/data/AssistantService.ts +4 -0
  82. package/src/features/assistant-message/components/parts/MessageSourcesContainer.tsx +5 -1
  83. package/src/features/assistant-message/data/AssistantMessage.ts +27 -2
  84. package/src/features/billing/i18n-keys.ts +7 -0
  85. package/src/features/billing/stripe-price/components/forms/PriceEditor.tsx +108 -2
  86. package/src/features/billing/stripe-price/components/forms/__tests__/PriceEditor.spec.tsx +182 -0
  87. package/src/features/billing/stripe-price/components/lists/PricesList.tsx +152 -25
  88. package/src/features/billing/stripe-price/components/lists/__tests__/PricesList.spec.tsx +268 -0
  89. package/src/features/billing/stripe-price/data/__tests__/stripe-price.transfer.spec.ts +265 -0
  90. package/src/features/billing/stripe-price/data/stripe-price.fields.ts +1 -0
  91. package/src/features/billing/stripe-price/data/stripe-price.transfer.ts +194 -0
  92. package/src/features/billing/stripe-price/hooks/useStripePriceTableStructure.tsx +66 -2
  93. package/src/features/content/data/content.interface.ts +1 -1
  94. package/src/features/handbook/HandbookPageModule.ts +41 -0
  95. package/src/features/handbook/HandbookSectionModule.ts +35 -0
  96. package/src/features/handbook/HandbookThreadMessageModule.ts +21 -0
  97. package/src/features/handbook/HandbookThreadModule.ts +29 -0
  98. package/src/features/handbook/components/containers/HandbookAdminContainer.tsx +17 -0
  99. package/src/features/handbook/components/containers/HandbookAskContainer.tsx +142 -0
  100. package/src/features/handbook/components/containers/HandbookAskSheet.tsx +149 -0
  101. package/src/features/handbook/components/containers/HandbookPageContainer.tsx +143 -0
  102. package/src/features/handbook/components/containers/__tests__/HandbookAskContainer.spec.tsx +268 -0
  103. package/src/features/handbook/components/containers/__tests__/HandbookAskSheet.spec.tsx +143 -0
  104. package/src/features/handbook/components/containers/__tests__/HandbookPageContainer.spec.tsx +288 -0
  105. package/src/features/handbook/components/details/HandbookPageDetails.tsx +27 -0
  106. package/src/features/handbook/components/details/HandbookPageNeighbours.tsx +72 -0
  107. package/src/features/handbook/components/details/HandbookPageToc.tsx +165 -0
  108. package/src/features/handbook/components/forms/HandbookSyncButton.tsx +43 -0
  109. package/src/features/handbook/components/forms/__tests__/HandbookSyncButton.spec.tsx +42 -0
  110. package/src/features/handbook/components/index.ts +12 -0
  111. package/src/features/handbook/components/lists/HandbookContents.tsx +196 -0
  112. package/src/features/handbook/components/lists/HandbookSectionGroup.tsx +153 -0
  113. package/src/features/handbook/components/lists/__tests__/HandbookContents.spec.tsx +247 -0
  114. package/src/features/handbook/components/parts/HandbookIndexStatus.tsx +62 -0
  115. package/src/features/handbook/components/parts/HandbookPageNavigator.tsx +143 -0
  116. package/src/features/handbook/contexts/HandbookContext.tsx +109 -0
  117. package/src/features/handbook/contexts/__tests__/HandbookContext.spec.tsx +131 -0
  118. package/src/features/handbook/data/HandbookPage.spec.ts +136 -0
  119. package/src/features/handbook/data/HandbookPage.ts +99 -0
  120. package/src/features/handbook/data/HandbookPageFields.ts +31 -0
  121. package/src/features/handbook/data/HandbookPageInput.ts +8 -0
  122. package/src/features/handbook/data/HandbookPageInterface.ts +29 -0
  123. package/src/features/handbook/data/HandbookPageService.ts +60 -0
  124. package/src/features/handbook/data/HandbookSection.spec.ts +58 -0
  125. package/src/features/handbook/data/HandbookSection.ts +73 -0
  126. package/src/features/handbook/data/HandbookSectionInput.ts +12 -0
  127. package/src/features/handbook/data/HandbookSectionInterface.ts +26 -0
  128. package/src/features/handbook/data/HandbookSectionService.ts +26 -0
  129. package/src/features/handbook/data/HandbookThread.ts +114 -0
  130. package/src/features/handbook/data/HandbookThreadInterface.ts +31 -0
  131. package/src/features/handbook/data/HandbookThreadMessage.ts +144 -0
  132. package/src/features/handbook/data/HandbookThreadMessageInterface.ts +35 -0
  133. package/src/features/handbook/data/HandbookThreadMessageService.ts +30 -0
  134. package/src/features/handbook/data/HandbookThreadService.ts +86 -0
  135. package/src/features/handbook/data/__tests__/HandbookThreadService.spec.ts +112 -0
  136. package/src/features/handbook/data/handbookDisplay.spec.ts +92 -0
  137. package/src/features/handbook/data/handbookDisplay.ts +55 -0
  138. package/src/features/handbook/data/index.ts +16 -0
  139. package/src/features/handbook/hooks/__tests__/useHandbookAsk.spec.ts +198 -0
  140. package/src/features/handbook/hooks/useHandbookAsk.ts +213 -0
  141. package/src/features/handbook/i18n-keys.ts +46 -0
  142. package/src/features/handbook/index.ts +7 -0
  143. package/src/features/how-to/components/forms/HowToEditor.tsx +0 -1
  144. package/src/features/how-to/components/forms/HowToReindexButton.spec.tsx +84 -0
  145. package/src/features/how-to/components/forms/HowToReindexButton.tsx +46 -0
  146. package/src/features/how-to/components/index.ts +1 -0
  147. package/src/features/how-to/components/lists/HowToList.tsx +5 -1
  148. package/src/features/how-to/data/HowTo.spec.ts +61 -1
  149. package/src/features/how-to/data/HowToService.ts +13 -0
  150. package/dist/chunk-6J2SQZ3G.js.map +0 -1
  151. package/dist/chunk-M5QAHSCY.mjs.map +0 -1
  152. package/dist/chunk-XNGI4HDZ.mjs.map +0 -1
  153. package/dist/chunk-YN4LOVL4.js.map +0 -1
  154. /package/dist/{BlockNoteEditor-NBHDEZ7W.mjs.map → BlockNoteEditor-SERY337S.mjs.map} +0 -0
  155. /package/dist/{BlockNoteViewer-TYLVTIUD.mjs.map → BlockNoteViewer-P2VKEFRN.mjs.map} +0 -0
  156. /package/dist/{chunk-IMSIVIIC.mjs.map → chunk-VZKJIAND.mjs.map} +0 -0
@@ -0,0 +1,29 @@
1
+ import { MessagesSquareIcon } from "lucide-react";
2
+ import { createJsonApiInclusion } from "../../core";
3
+ import { ModuleFactory } from "../../permissions";
4
+ import { HandbookThread } from "./data/HandbookThread";
5
+
6
+ /**
7
+ * A persisted handbook conversation.
8
+ *
9
+ * It carries no `pageUrl`: a thread is selected inside the ask surface
10
+ * (`/administration/handbook/chat`), never routed to on its own, and a
11
+ * `pageUrl` here would add a phantom entry to the RBAC path map.
12
+ *
13
+ * `lists` deliberately narrows to the title — the sidebar renders nothing else,
14
+ * and side-loading every message of every thread into the list payload is what
15
+ * the single-resource endpoint is for.
16
+ */
17
+ export const HandbookThreadModule = (factory: ModuleFactory) =>
18
+ factory({
19
+ moduleId: "aea692c4-7163-49bd-9ddc-1052cc06fa14",
20
+ name: "handbookthreads",
21
+ model: HandbookThread,
22
+ icon: MessagesSquareIcon,
23
+ identifier: ["title"],
24
+ inclusions: {
25
+ lists: {
26
+ fields: [createJsonApiInclusion("handbookthreads", ["title"])],
27
+ },
28
+ },
29
+ });
@@ -0,0 +1,17 @@
1
+ "use client";
2
+
3
+ import HandbookContents from "../lists/HandbookContents";
4
+
5
+ /**
6
+ * The administration route's entry point for the manual.
7
+ *
8
+ * `RoundPageContainer` moved down into `HandbookContents`, which is the only
9
+ * component that can supply it: the container's title bar reads its heading
10
+ * from `useSharedContext()`, so `HandbookProvider` has to wrap it, and the
11
+ * provider's `functions` (the ask launcher, Sincronizza and the status line)
12
+ * are built from the state `HandbookContents` holds. `HandbookAskContainer`
13
+ * composes its own surface the same way.
14
+ */
15
+ export default function HandbookAdminContainer() {
16
+ return <HandbookContents />;
17
+ }
@@ -0,0 +1,142 @@
1
+ "use client";
2
+
3
+ import { MessagesSquareIcon, Sparkles } from "lucide-react";
4
+ import { useTranslations } from "next-intl";
5
+ import { useMemo } from "react";
6
+
7
+ import { RoundPageContainer } from "../../../../components/containers/RoundPageContainer";
8
+ import { SectionHeader } from "../../../../components/typography";
9
+ import { Modules } from "../../../../core";
10
+ import { AssistantComposer } from "../../../assistant/components/parts/AssistantComposer";
11
+ import { AssistantSidebar } from "../../../assistant/components/parts/AssistantSidebar";
12
+ import { AssistantThread } from "../../../assistant/components/parts/AssistantThread";
13
+ import { HandbookProvider } from "../../contexts/HandbookContext";
14
+ import { useHandbookAsk } from "../../hooks/useHandbookAsk";
15
+
16
+ /**
17
+ * The handbook ask surface.
18
+ *
19
+ * Composition is copied from `AssistantPageContainer` — the full-page assistant
20
+ * — top to bottom: the thread list lives in the page's details panel (which
21
+ * renders it on the trailing side) and the conversation fills the card. The
22
+ * transcript is rendered by the SAME components as every other chat in the
23
+ * product (`AssistantSidebar`, `AssistantThread`),
24
+ * which take their data as plain props and are typed against the structural
25
+ * `AssistantInterface` / `AssistantMessageInterface` that `HandbookThread` and
26
+ * `HandbookThreadMessage` implement.
27
+ *
28
+ * Two deliberate deviations from that reference, both forced by the data source:
29
+ *
30
+ * 1. It cannot MOUNT `AssistantPageContainer`, which reads
31
+ * `useAssistantContext()`. That provider is bound to the `Assistant` entity,
32
+ * which is company scoped, and a platform administrator has no company — so
33
+ * the handbook owns its own threads, and the state that provider would hold
34
+ * lives in `useHandbookAsk` — shared with the ask sheet, which is the same
35
+ * conversation on a different surface.
36
+ * 2. The composer is the plain `AssistantComposer`, not the BlockNote one. The
37
+ * BlockNote composer exists so a first message can carry `@`-mentions of
38
+ * product entities; the handbook agent answers from documentation pages and
39
+ * has no entities to mention, so that machinery would be dead weight.
40
+ *
41
+ * Every label is passed in from the `handbook.*` namespace. The assistant's own
42
+ * copy ("New assistant", "no assistants yet", "ask me about a matter, a client,
43
+ * a document…") is wrong on this surface, so the shared parts take optional
44
+ * label props that default to their existing text.
45
+ */
46
+
47
+ type HandbookAskContainerProps = {
48
+ /** Thread to open on mount, when the route carries one. */
49
+ handbookThreadId?: string;
50
+ };
51
+
52
+ export function HandbookAskContainer({ handbookThreadId }: HandbookAskContainerProps = {}) {
53
+ const t = useTranslations();
54
+
55
+ // The conversation itself lives in `useHandbookAsk`, because the ask sheet
56
+ // holds the same one. The route keeps the default `syncUrl: true`, so the
57
+ // address bar still follows the open thread exactly as it did when this
58
+ // component owned the state.
59
+ const { threads, thread, messages, sending, ask, selectThread, startNew } = useHandbookAsk({ handbookThreadId });
60
+
61
+ const showThread = !!thread || sending || messages.length > 0;
62
+ // Memoised, and not as a micro-optimisation: it was a fresh object on every
63
+ // render being fed into RoundPageContainer, which is the shape that produces
64
+ // React's "Maximum update depth exceeded".
65
+ const details = useMemo(
66
+ () => (
67
+ // The panel supplies its own padding and border; cancel both so the list
68
+ // reads as the panel body rather than a card inside it.
69
+ <div className="-m-4 flex h-full min-h-0 [&>aside]:w-full [&>aside]:border-e-0 [&>aside]:bg-transparent">
70
+ <AssistantSidebar
71
+ threads={threads}
72
+ activeId={thread?.id}
73
+ onSelect={selectThread}
74
+ onNew={startNew}
75
+ newLabel={t("handbook.chat.new")}
76
+ emptyLabel={t("handbook.chat.empty_sidebar")}
77
+ />
78
+ </div>
79
+ ),
80
+ [selectThread, startNew, t, thread?.id, threads],
81
+ );
82
+
83
+ return (
84
+ // `RoundPageContainer` reads the page heading from the shared context, and
85
+ // `forceHeader` is what renders it — without a provider the hook throws.
86
+ // `HandbookProvider` is that provider for every handbook surface; the ask
87
+ // is the one that overrides the title, because it is a conversation about
88
+ // the manual rather than a page of it.
89
+ <HandbookProvider titleType={t("handbook.chat.title")}>
90
+ <RoundPageContainer
91
+ module={Modules.HandbookPage}
92
+ id={thread?.id}
93
+ fullWidth
94
+ forceHeader
95
+ defaultDetailsOpen
96
+ detailsTitle={t("handbook.chat.list_title")}
97
+ detailsIcon={<MessagesSquareIcon />}
98
+ details={details}
99
+ >
100
+ {/* No `rounded-lg border` here. `AssistantPageContainer` has them on
101
+ this wrapper, but RoundPageContainer already draws the card's border
102
+ around this child, so copying them doubles it. a360's own chat
103
+ content — ConversationMessageContainer — is a bare
104
+ `flex h-full w-full min-h-0 flex-col`, and that is the shape this
105
+ app's pages are built to. */}
106
+ <div className="flex h-full min-h-0 w-full flex-col overflow-hidden">
107
+ {showThread ? (
108
+ <>
109
+ {/* No per-thread header. `RoundPageContainer` already renders the
110
+ page header above this card, and a second title row inside it
111
+ is the double header. `AssistantThreadHeader` also carries
112
+ rename/delete, which this surface does not offer —
113
+ `ConversationMessageContainer` has no such row either. */}
114
+ <AssistantThread
115
+ messages={messages}
116
+ sending={sending}
117
+ status={t("handbook.chat.pending")}
118
+ onSelectFollowUp={ask}
119
+ />
120
+ </>
121
+ ) : (
122
+ <div className="flex flex-1 items-center justify-center p-10">
123
+ <div className="flex max-w-2xl flex-col items-center text-center">
124
+ <span className="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">
125
+ <Sparkles className="h-5 w-5" />
126
+ </span>
127
+ <SectionHeader>{t("handbook.chat.empty_state.title")}</SectionHeader>
128
+ <p className="text-muted-foreground mt-1 text-sm">{t("handbook.chat.empty_state.subtitle")}</p>
129
+ </div>
130
+ </div>
131
+ )}
132
+ <AssistantComposer
133
+ onSend={ask}
134
+ disabled={sending}
135
+ placeholder={t("handbook.chat.placeholder")}
136
+ sendLabel={t("handbook.chat.ask")}
137
+ />
138
+ </div>
139
+ </RoundPageContainer>
140
+ </HandbookProvider>
141
+ );
142
+ }
@@ -0,0 +1,149 @@
1
+ "use client";
2
+
3
+ import { MessagesSquareIcon, Sparkles } from "lucide-react";
4
+ import { useTranslations } from "next-intl";
5
+ import Link from "next/link";
6
+ import { useCallback, useState } from "react";
7
+
8
+ import { SectionHeader } from "../../../../components/typography";
9
+ import { Modules } from "../../../../core";
10
+ import { usePageUrlGenerator } from "../../../../hooks";
11
+ import {
12
+ Button,
13
+ DropdownMenu,
14
+ DropdownMenuContent,
15
+ DropdownMenuItem,
16
+ DropdownMenuTrigger,
17
+ Sheet,
18
+ SheetContent,
19
+ SheetHeader,
20
+ SheetTitle,
21
+ SheetTrigger,
22
+ Switch,
23
+ } from "../../../../shadcnui";
24
+ import { AssistantComposer } from "../../../assistant/components/parts/AssistantComposer";
25
+ import { AssistantThread } from "../../../assistant/components/parts/AssistantThread";
26
+ import { useHandbookAsk } from "../../hooks/useHandbookAsk";
27
+
28
+ /**
29
+ * The ask, reachable from every handbook surface.
30
+ *
31
+ * It is the same conversation as `/administration/handbook/chat`, not a second
32
+ * one: both mount `useHandbookAsk`, so the threads listed here are the threads
33
+ * listed there and a question asked in the sheet is readable on the route. Only
34
+ * two things differ.
35
+ *
36
+ * 1. No url sync. A sheet floats over the page a reader is on; rewriting the
37
+ * address under it would send them somewhere else the moment they close it.
38
+ * `syncUrl: false` is exactly that difference.
39
+ * 2. No thread sidebar. The full list belongs to the route — the sheet carries
40
+ * the five most recent threads in a menu and a link to the rest, which is
41
+ * what a side panel has room for.
42
+ *
43
+ * On the reader the sheet also knows which page is open, and offers to confine
44
+ * the answer to it. The chip is off by default: a reader asking from inside a
45
+ * page usually still wants the whole manual to answer, and the narrow search is
46
+ * the deliberate choice.
47
+ */
48
+ export function HandbookAskSheet({ handbookPageId }: HandbookAskSheetProps = {}) {
49
+ const t = useTranslations();
50
+ const generateUrl = usePageUrlGenerator();
51
+
52
+ const { threads, thread, messages, sending, ask, selectThread } = useHandbookAsk({ syncUrl: false });
53
+ const [scoped, setScoped] = useState(false);
54
+
55
+ const chatUrl = `${generateUrl({ page: Modules.HandbookPage })}/chat`;
56
+ const showThread = !!thread || sending || messages.length > 0;
57
+
58
+ const send = useCallback(
59
+ async (question: string) => {
60
+ await ask(question, { handbookPageId: scoped ? handbookPageId : undefined });
61
+ },
62
+ [ask, handbookPageId, scoped],
63
+ );
64
+
65
+ return (
66
+ <Sheet>
67
+ {/* `render`, never a nested <Button>: the trigger draws its own <button>,
68
+ and wrapping one inside it is invalid HTML. */}
69
+ <SheetTrigger render={<Button variant="outline" />}>
70
+ <MessagesSquareIcon className="me-1 h-4 w-4" />
71
+ {t("handbook.ask.open")}
72
+ </SheetTrigger>
73
+ <SheetContent side="end" className="flex w-full flex-col sm:max-w-lg">
74
+ {/* pe-10 clears SheetContent's own close button, which sits at the
75
+ inline end of the same row. */}
76
+ <SheetHeader className="flex-row items-center gap-2 pe-10">
77
+ <SheetTitle className="flex-1">{t("handbook.chat.title")}</SheetTitle>
78
+
79
+ <DropdownMenu>
80
+ <DropdownMenuTrigger render={<Button variant="ghost" size="sm" />}>
81
+ {t("handbook.ask.recent")}
82
+ </DropdownMenuTrigger>
83
+ <DropdownMenuContent align="end">
84
+ {threads.slice(0, 5).map((item) => (
85
+ <DropdownMenuItem key={item.id} onClick={() => void selectThread(item.id)}>
86
+ {item.title}
87
+ </DropdownMenuItem>
88
+ ))}
89
+ <DropdownMenuItem>
90
+ <Link href={chatUrl} className="text-primary w-full font-medium">
91
+ {t("handbook.ask.all")}
92
+ </Link>
93
+ </DropdownMenuItem>
94
+ </DropdownMenuContent>
95
+ </DropdownMenu>
96
+
97
+ {thread && (
98
+ <Link href={`${chatUrl}/${thread.id}`} className="text-primary font-medium">
99
+ {t("handbook.ask.expand")}
100
+ </Link>
101
+ )}
102
+ </SheetHeader>
103
+
104
+ <div className="flex min-h-0 w-full flex-1 flex-col overflow-hidden">
105
+ {showThread ? (
106
+ <AssistantThread
107
+ messages={messages}
108
+ sending={sending}
109
+ status={t("handbook.chat.pending")}
110
+ onSelectFollowUp={send}
111
+ />
112
+ ) : (
113
+ <div className="flex flex-1 items-center justify-center p-6">
114
+ <div className="flex max-w-sm flex-col items-center text-center">
115
+ <span className="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">
116
+ <Sparkles className="h-5 w-5" />
117
+ </span>
118
+ <SectionHeader>{t("handbook.chat.empty_state.title")}</SectionHeader>
119
+ <p className="text-muted-foreground mt-1 text-sm">{t("handbook.chat.empty_state.subtitle")}</p>
120
+ </div>
121
+ </div>
122
+ )}
123
+
124
+ {handbookPageId && (
125
+ <div className="flex items-center gap-2 border-t px-4 pt-3">
126
+ <Switch checked={scoped} onCheckedChange={setScoped} aria-label={t("handbook.ask.thisPageOnly")} />
127
+ <span className="text-muted-foreground text-xs">{t("handbook.ask.thisPageOnly")}</span>
128
+ </div>
129
+ )}
130
+
131
+ <AssistantComposer
132
+ onSend={send}
133
+ disabled={sending}
134
+ placeholder={t("handbook.chat.placeholder")}
135
+ sendLabel={t("handbook.chat.ask")}
136
+ />
137
+ </div>
138
+ </SheetContent>
139
+ </Sheet>
140
+ );
141
+ }
142
+
143
+ type HandbookAskSheetProps = {
144
+ /**
145
+ * The page the reader has open. Present only on the reader; when set, the
146
+ * sheet offers to confine the next question to that page.
147
+ */
148
+ handbookPageId?: string;
149
+ };
@@ -0,0 +1,143 @@
1
+ "use client";
2
+
3
+ import { useTranslations } from "next-intl";
4
+ import { useEffect, useMemo, useState } from "react";
5
+ import { RoundPageContainer } from "../../../../components";
6
+ import { Modules } from "../../../../core";
7
+ import { HandbookProvider } from "../../contexts/HandbookContext";
8
+ import { HandbookPageInterface } from "../../data/HandbookPageInterface";
9
+ import { HandbookPageService } from "../../data/HandbookPageService";
10
+ import { HandbookSectionInterface } from "../../data/HandbookSectionInterface";
11
+ import { HandbookSectionService } from "../../data/HandbookSectionService";
12
+ import { HandbookPageDetails } from "../details/HandbookPageDetails";
13
+ import { HandbookPageNeighbours } from "../details/HandbookPageNeighbours";
14
+ import { HandbookPageToc } from "../details/HandbookPageToc";
15
+ import { HandbookPageNavigator } from "../parts/HandbookPageNavigator";
16
+ import { HandbookAskSheet } from "./HandbookAskSheet";
17
+
18
+ type HandbookPageContainerProps = {
19
+ handbookPageId: string;
20
+ };
21
+
22
+ /** The element the heading index reads its headings out of. */
23
+ const CONTENT_CONTAINER_ID = "handbook-page-content";
24
+
25
+ /**
26
+ * Fetches one page client-side from its id.
27
+ *
28
+ * The sibling how-to detail uses a server page that dehydrates into a provider.
29
+ * This one does not, for the same reason the ask surface does not: a handbook
30
+ * page is read-only and never edited, so there is no shared mutable state for a
31
+ * provider to hold, and an id string crosses the server/client boundary without
32
+ * any dehydrate/rehydrate ceremony.
33
+ *
34
+ * Three loads, deliberately separate: the page itself, the whole ordered page
35
+ * list (the navigator and the prev/next pair need it) and the sections. The
36
+ * section load has its own `try` — a failing sections endpoint degrades the
37
+ * navigator to raw keys, and must never take the page being read down with it.
38
+ */
39
+ export default function HandbookPageContainer({ handbookPageId }: HandbookPageContainerProps) {
40
+ const t = useTranslations();
41
+ const [handbookPage, setHandbookPage] = useState<HandbookPageInterface | undefined>(undefined);
42
+ const [pages, setPages] = useState<HandbookPageInterface[]>([]);
43
+ const [sections, setSections] = useState<HandbookSectionInterface[]>([]);
44
+ const [isLoading, setIsLoading] = useState(true);
45
+
46
+ useEffect(() => {
47
+ let cancelled = false;
48
+
49
+ const load = async () => {
50
+ setIsLoading(true);
51
+ try {
52
+ const page = await HandbookPageService.findOne({ id: handbookPageId });
53
+ if (!cancelled) setHandbookPage(page);
54
+ } finally {
55
+ if (!cancelled) setIsLoading(false);
56
+ }
57
+ };
58
+
59
+ void load();
60
+
61
+ return () => {
62
+ cancelled = true;
63
+ };
64
+ }, [handbookPageId]);
65
+
66
+ useEffect(() => {
67
+ let cancelled = false;
68
+
69
+ const loadPages = async () => {
70
+ const allPages = await HandbookPageService.findMany({ fetchAll: true });
71
+ if (!cancelled) setPages(allPages);
72
+ };
73
+
74
+ const loadSections = async () => {
75
+ try {
76
+ const allSections = await HandbookSectionService.findMany();
77
+ if (!cancelled) setSections(allSections);
78
+ } catch {
79
+ if (!cancelled) setSections([]);
80
+ }
81
+ };
82
+
83
+ void loadPages();
84
+ void loadSections();
85
+
86
+ return () => {
87
+ cancelled = true;
88
+ };
89
+ }, []);
90
+
91
+ const section = useMemo(
92
+ () => sections.find((candidate) => candidate.key === handbookPage?.section),
93
+ [sections, handbookPage],
94
+ );
95
+
96
+ return (
97
+ <HandbookProvider
98
+ page={handbookPage}
99
+ section={section}
100
+ functions={handbookPage ? <HandbookAskSheet handbookPageId={handbookPage.id} /> : undefined}
101
+ >
102
+ {/*
103
+ `fullWidth` on the READER, unlike the contents page.
104
+
105
+ The container's `max-w-6xl` exists to hold a reading measure, and the
106
+ two rails are not reading — cramming navigator, prose and heading index
107
+ into that one column squeezes the prose to a third of it. So the reader
108
+ takes the full width and caps only its centre column, which is where the
109
+ measure belongs.
110
+ */}
111
+ <RoundPageContainer module={Modules.HandbookPage} fullWidth forceHeader>
112
+ {isLoading ? null : handbookPage ? (
113
+ /*
114
+ `items-start` is what makes `sticky` work on the two side columns:
115
+ a grid item stretches to the row's height by default, so a sticky
116
+ child inside it has nowhere to travel and the whole page scrolls
117
+ instead. With `items-start` each column is only as tall as its
118
+ content, and the rails hold their place while the page moves.
119
+
120
+ Each rail wrapper owns the scrolling — the navigator carries no
121
+ `max-h` of its own, so there is exactly one scroll container per
122
+ rail rather than two nested ones.
123
+ */
124
+ <div className="grid items-start gap-8 p-4 lg:grid-cols-[240px_minmax(0,1fr)_240px]">
125
+ <div className="sticky top-4 hidden max-h-[calc(100svh-8rem)] overflow-y-auto lg:block">
126
+ <HandbookPageNavigator pages={pages} sections={sections} currentId={handbookPage.id} />
127
+ </div>
128
+ {/* The measure lives HERE, on the prose, not on the whole page. */}
129
+ <div id={CONTENT_CONTAINER_ID} className="mx-auto w-full max-w-3xl min-w-0">
130
+ <HandbookPageDetails handbookPage={handbookPage} />
131
+ <HandbookPageNeighbours pages={pages} currentId={handbookPage.id} />
132
+ </div>
133
+ <div className="sticky top-4 hidden max-h-[calc(100svh-8rem)] overflow-y-auto lg:block">
134
+ <HandbookPageToc containerId={CONTENT_CONTAINER_ID} />
135
+ </div>
136
+ </div>
137
+ ) : (
138
+ <span className="text-muted-foreground text-sm">{t("handbook.notFound")}</span>
139
+ )}
140
+ </RoundPageContainer>
141
+ </HandbookProvider>
142
+ );
143
+ }