@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.
- package/dist/{AssistantInterface-DwdBzS9f.d.ts → AssistantInterface-RvZzlcA_.d.ts} +2 -0
- package/dist/{AssistantInterface-DMh-zApr.d.mts → AssistantInterface-jL56dyNc.d.mts} +2 -0
- package/dist/{AssistantMessageInterface-0zyVingf.d.ts → AssistantMessageInterface-CuaMDs2i.d.ts} +1 -1
- package/dist/{AssistantMessageInterface-ddyG114R.d.mts → AssistantMessageInterface-Du3ShThp.d.mts} +1 -1
- package/dist/{BlockNoteEditor-XOVNTXDY.js → BlockNoteEditor-CFVSHS46.js} +19 -19
- package/dist/{BlockNoteEditor-XOVNTXDY.js.map → BlockNoteEditor-CFVSHS46.js.map} +1 -1
- package/dist/{BlockNoteEditor-NBHDEZ7W.mjs → BlockNoteEditor-SERY337S.mjs} +4 -4
- package/dist/{BlockNoteViewer-C2UUOBAF.js → BlockNoteViewer-CWFPYYIU.js} +3 -3
- package/dist/{BlockNoteViewer-C2UUOBAF.js.map → BlockNoteViewer-CWFPYYIU.js.map} +1 -1
- package/dist/{BlockNoteViewer-TYLVTIUD.mjs → BlockNoteViewer-P2VKEFRN.mjs} +2 -2
- package/dist/{ai-connection.module-28f-hux0.d.mts → HandbookSectionInterface-BOxIdBdd.d.mts} +54 -1
- package/dist/{ai-connection.module-Bqwh_DnR.d.ts → HandbookSectionInterface-CAXxbDw0.d.ts} +54 -1
- package/dist/{auth.interface-B6ZQLGjS.d.ts → auth.interface-BC1jp_ni.d.ts} +1 -1
- package/dist/{auth.interface-jYD8Gc4P.d.mts → auth.interface-dR2-6gPM.d.mts} +1 -1
- package/dist/billing/index.d.mts +27 -0
- package/dist/billing/index.d.ts +27 -0
- package/dist/billing/index.js +697 -450
- package/dist/billing/index.js.map +1 -1
- package/dist/billing/index.mjs +282 -35
- package/dist/billing/index.mjs.map +1 -1
- package/dist/{chunk-6J2SQZ3G.js → chunk-IB7DTXWG.js} +847 -129
- package/dist/chunk-IB7DTXWG.js.map +1 -0
- package/dist/{chunk-YN4LOVL4.js → chunk-LVE3BJ6K.js} +2106 -1109
- package/dist/chunk-LVE3BJ6K.js.map +1 -0
- package/dist/{chunk-M5QAHSCY.mjs → chunk-OSFYCSTW.mjs} +725 -7
- package/dist/chunk-OSFYCSTW.mjs.map +1 -0
- package/dist/{chunk-XNGI4HDZ.mjs → chunk-UQXE3SME.mjs} +2100 -1103
- package/dist/chunk-UQXE3SME.mjs.map +1 -0
- package/dist/{chunk-IMSIVIIC.mjs → chunk-VZKJIAND.mjs} +2 -2
- package/dist/{chunk-V6CGCARD.js → chunk-XH4IURUD.js} +7 -7
- package/dist/{chunk-V6CGCARD.js.map → chunk-XH4IURUD.js.map} +1 -1
- package/dist/client/index.d.mts +2 -2
- package/dist/client/index.d.ts +2 -2
- package/dist/client/index.js +4 -4
- package/dist/client/index.mjs +3 -3
- package/dist/components/index.d.mts +258 -7
- package/dist/components/index.d.ts +258 -7
- package/dist/components/index.js +30 -4
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +29 -3
- package/dist/contexts/index.d.mts +17 -5
- package/dist/contexts/index.d.ts +17 -5
- package/dist/contexts/index.js +4 -4
- package/dist/contexts/index.mjs +3 -3
- package/dist/core/index.d.mts +493 -12
- package/dist/core/index.d.ts +493 -12
- package/dist/core/index.js +38 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +37 -1
- package/dist/features/help/index.js +37 -37
- package/dist/features/help/index.mjs +3 -3
- package/dist/features/tokenusage/index.js +101 -101
- package/dist/features/tokenusage/index.mjs +3 -3
- package/dist/index.d.mts +7 -7
- package/dist/index.d.ts +7 -7
- package/dist/index.js +39 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +38 -2
- package/dist/{notification.interface-DYF56CtE.d.ts → notification.interface-CjzV_KII.d.ts} +1 -1
- package/dist/{notification.interface-BWOCvKRP.d.mts → notification.interface-MnRt1Tjm.d.mts} +1 -1
- package/dist/{s3.service-D_dswjgz.d.mts → s3.service-C0Yq2Sx3.d.mts} +2 -2
- package/dist/{s3.service-DserU9VK.d.ts → s3.service-CUGarZs3.d.ts} +2 -2
- package/dist/server/index.d.mts +3 -3
- package/dist/server/index.d.ts +3 -3
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +1 -1
- package/dist/{useSocket-BZG9oX-L.d.mts → useSocket-BcH7l7B8.d.mts} +1 -1
- package/dist/{useSocket-CiyTtYFj.d.ts → useSocket-DCZd_eJk.d.ts} +1 -1
- package/package.json +1 -1
- package/src/components/containers/ReactMarkdownContainer.tsx +61 -11
- package/src/components/index.ts +1 -0
- package/src/core/index.ts +5 -0
- package/src/core/registry/ModuleRegistry.ts +4 -0
- package/src/features/assistant/components/parts/AssistantComposer.tsx +22 -3
- package/src/features/assistant/components/parts/AssistantSidebar.tsx +13 -3
- package/src/features/assistant/components/parts/AssistantThreadHeader.tsx +10 -3
- package/src/features/assistant/contexts/AssistantContext.tsx +30 -3
- package/src/features/assistant/contexts/__tests__/AssistantContext.spec.tsx +32 -0
- package/src/features/assistant/data/Assistant.ts +2 -0
- package/src/features/assistant/data/AssistantInterface.ts +2 -0
- package/src/features/assistant/data/AssistantService.ts +4 -0
- package/src/features/assistant-message/components/parts/MessageSourcesContainer.tsx +5 -1
- package/src/features/assistant-message/data/AssistantMessage.ts +27 -2
- package/src/features/billing/i18n-keys.ts +7 -0
- package/src/features/billing/stripe-price/components/forms/PriceEditor.tsx +108 -2
- package/src/features/billing/stripe-price/components/forms/__tests__/PriceEditor.spec.tsx +182 -0
- package/src/features/billing/stripe-price/components/lists/PricesList.tsx +152 -25
- package/src/features/billing/stripe-price/components/lists/__tests__/PricesList.spec.tsx +268 -0
- package/src/features/billing/stripe-price/data/__tests__/stripe-price.transfer.spec.ts +265 -0
- package/src/features/billing/stripe-price/data/stripe-price.fields.ts +1 -0
- package/src/features/billing/stripe-price/data/stripe-price.transfer.ts +194 -0
- package/src/features/billing/stripe-price/hooks/useStripePriceTableStructure.tsx +66 -2
- package/src/features/content/data/content.interface.ts +1 -1
- package/src/features/handbook/HandbookPageModule.ts +41 -0
- package/src/features/handbook/HandbookSectionModule.ts +35 -0
- package/src/features/handbook/HandbookThreadMessageModule.ts +21 -0
- package/src/features/handbook/HandbookThreadModule.ts +29 -0
- package/src/features/handbook/components/containers/HandbookAdminContainer.tsx +17 -0
- package/src/features/handbook/components/containers/HandbookAskContainer.tsx +142 -0
- package/src/features/handbook/components/containers/HandbookAskSheet.tsx +149 -0
- package/src/features/handbook/components/containers/HandbookPageContainer.tsx +143 -0
- package/src/features/handbook/components/containers/__tests__/HandbookAskContainer.spec.tsx +268 -0
- package/src/features/handbook/components/containers/__tests__/HandbookAskSheet.spec.tsx +143 -0
- package/src/features/handbook/components/containers/__tests__/HandbookPageContainer.spec.tsx +288 -0
- package/src/features/handbook/components/details/HandbookPageDetails.tsx +27 -0
- package/src/features/handbook/components/details/HandbookPageNeighbours.tsx +72 -0
- package/src/features/handbook/components/details/HandbookPageToc.tsx +165 -0
- package/src/features/handbook/components/forms/HandbookSyncButton.tsx +43 -0
- package/src/features/handbook/components/forms/__tests__/HandbookSyncButton.spec.tsx +42 -0
- package/src/features/handbook/components/index.ts +12 -0
- package/src/features/handbook/components/lists/HandbookContents.tsx +196 -0
- package/src/features/handbook/components/lists/HandbookSectionGroup.tsx +153 -0
- package/src/features/handbook/components/lists/__tests__/HandbookContents.spec.tsx +247 -0
- package/src/features/handbook/components/parts/HandbookIndexStatus.tsx +62 -0
- package/src/features/handbook/components/parts/HandbookPageNavigator.tsx +143 -0
- package/src/features/handbook/contexts/HandbookContext.tsx +109 -0
- package/src/features/handbook/contexts/__tests__/HandbookContext.spec.tsx +131 -0
- package/src/features/handbook/data/HandbookPage.spec.ts +136 -0
- package/src/features/handbook/data/HandbookPage.ts +99 -0
- package/src/features/handbook/data/HandbookPageFields.ts +31 -0
- package/src/features/handbook/data/HandbookPageInput.ts +8 -0
- package/src/features/handbook/data/HandbookPageInterface.ts +29 -0
- package/src/features/handbook/data/HandbookPageService.ts +60 -0
- package/src/features/handbook/data/HandbookSection.spec.ts +58 -0
- package/src/features/handbook/data/HandbookSection.ts +73 -0
- package/src/features/handbook/data/HandbookSectionInput.ts +12 -0
- package/src/features/handbook/data/HandbookSectionInterface.ts +26 -0
- package/src/features/handbook/data/HandbookSectionService.ts +26 -0
- package/src/features/handbook/data/HandbookThread.ts +114 -0
- package/src/features/handbook/data/HandbookThreadInterface.ts +31 -0
- package/src/features/handbook/data/HandbookThreadMessage.ts +144 -0
- package/src/features/handbook/data/HandbookThreadMessageInterface.ts +35 -0
- package/src/features/handbook/data/HandbookThreadMessageService.ts +30 -0
- package/src/features/handbook/data/HandbookThreadService.ts +86 -0
- package/src/features/handbook/data/__tests__/HandbookThreadService.spec.ts +112 -0
- package/src/features/handbook/data/handbookDisplay.spec.ts +92 -0
- package/src/features/handbook/data/handbookDisplay.ts +55 -0
- package/src/features/handbook/data/index.ts +16 -0
- package/src/features/handbook/hooks/__tests__/useHandbookAsk.spec.ts +198 -0
- package/src/features/handbook/hooks/useHandbookAsk.ts +213 -0
- package/src/features/handbook/i18n-keys.ts +46 -0
- package/src/features/handbook/index.ts +7 -0
- package/src/features/how-to/components/forms/HowToEditor.tsx +0 -1
- package/src/features/how-to/components/forms/HowToReindexButton.spec.tsx +84 -0
- package/src/features/how-to/components/forms/HowToReindexButton.tsx +46 -0
- package/src/features/how-to/components/index.ts +1 -0
- package/src/features/how-to/components/lists/HowToList.tsx +5 -1
- package/src/features/how-to/data/HowTo.spec.ts +61 -1
- package/src/features/how-to/data/HowToService.ts +13 -0
- package/dist/chunk-6J2SQZ3G.js.map +0 -1
- package/dist/chunk-M5QAHSCY.mjs.map +0 -1
- package/dist/chunk-XNGI4HDZ.mjs.map +0 -1
- package/dist/chunk-YN4LOVL4.js.map +0 -1
- /package/dist/{BlockNoteEditor-NBHDEZ7W.mjs.map → BlockNoteEditor-SERY337S.mjs.map} +0 -0
- /package/dist/{BlockNoteViewer-TYLVTIUD.mjs.map → BlockNoteViewer-P2VKEFRN.mjs.map} +0 -0
- /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
|
+
}
|