@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
|
@@ -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({
|
|
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">
|
|
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?: {
|
|
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 (
|
|
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
|
-
|
|
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:
|
|
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 =
|
|
192
|
+
msg._isOptimistic = params.isOptimistic ?? false;
|
|
168
193
|
return msg;
|
|
169
194
|
}
|
|
170
195
|
}
|
|
@@ -36,6 +36,7 @@ export const BILLING_ADMIN_I18N_KEYS = [
|
|
|
36
36
|
"billing.admin.prices.create",
|
|
37
37
|
"billing.admin.prices.empty.title",
|
|
38
38
|
"billing.admin.prices.empty.description",
|
|
39
|
+
"billing.admin.prices.fields.actions",
|
|
39
40
|
"billing.admin.prices.fields.product",
|
|
40
41
|
"billing.admin.prices.fields.nickname",
|
|
41
42
|
"billing.admin.prices.fields.amount",
|
|
@@ -79,6 +80,12 @@ export const BILLING_ADMIN_I18N_KEYS = [
|
|
|
79
80
|
"billing.admin.prices.immutability.description",
|
|
80
81
|
"billing.admin.prices.actions.addFeature",
|
|
81
82
|
"billing.admin.prices.actions.removeFeature",
|
|
83
|
+
"billing.admin.prices.actions.clone",
|
|
84
|
+
"billing.admin.prices.actions.export",
|
|
85
|
+
"billing.admin.prices.actions.import",
|
|
86
|
+
"billing.admin.prices.import.errors.title",
|
|
87
|
+
"billing.admin.prices.import.errors.invalid",
|
|
88
|
+
"billing.admin.prices.import.warnings.unknownFeatures",
|
|
82
89
|
"billing.admin.prices.archive.title",
|
|
83
90
|
"billing.admin.prices.archive.description",
|
|
84
91
|
"billing.admin.prices.archive.confirm",
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
4
4
|
import { PlusIcon, XIcon } from "lucide-react";
|
|
5
5
|
import { useTranslations } from "next-intl";
|
|
6
|
-
import { ReactNode, useCallback, useEffect, useMemo, useState } from "react";
|
|
6
|
+
import { ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
7
7
|
import { useForm } from "react-hook-form";
|
|
8
8
|
import { v4 } from "uuid";
|
|
9
9
|
import { z } from "zod";
|
|
@@ -11,13 +11,27 @@ import { EditorSheet, FieldLabel, FormCheckbox, FormInput, FormSelect, FormTexta
|
|
|
11
11
|
import { Modules } from "../../../../../core";
|
|
12
12
|
import { useI18nRouter } from "../../../../../i18n";
|
|
13
13
|
import { Alert, AlertDescription, AlertTitle, Button, Checkbox, Input } from "../../../../../shadcnui";
|
|
14
|
+
import { showToast } from "../../../../../utils/toast";
|
|
14
15
|
import { FeatureInterface, FeatureService } from "../../../../feature";
|
|
15
16
|
import { priceLabel, usePriceContext } from "../../../contexts/PriceContext";
|
|
16
17
|
import { StripePriceInput, StripePriceInterface } from "../../data/stripe-price.interface";
|
|
18
|
+
import { StripePriceSeed } from "../../data/stripe-price.transfer";
|
|
17
19
|
|
|
18
20
|
export type PriceEditorProps = {
|
|
19
21
|
productId?: string;
|
|
20
22
|
price?: StripePriceInterface;
|
|
23
|
+
/**
|
|
24
|
+
* Pre-fills a NEW price — the Clone (a row in the prices list) and Import (a
|
|
25
|
+
* JSON document) paths both hand over the same seed.
|
|
26
|
+
*
|
|
27
|
+
* The seed deliberately carries NO identity: no Stripe price id, no internal
|
|
28
|
+
* id and no product id. A clone/import is a brand-new price, minted fresh by
|
|
29
|
+
* Stripe and attached to whatever product this editor is mounted for
|
|
30
|
+
* (`productId` / the context), so copying any identity across would either
|
|
31
|
+
* collide or silently re-point the new price at the source environment's
|
|
32
|
+
* objects. Seeding therefore never makes this an edit — see `isEdit` below.
|
|
33
|
+
*/
|
|
34
|
+
seed?: StripePriceSeed;
|
|
21
35
|
propagateChanges?: (price: StripePriceInterface) => void;
|
|
22
36
|
onSuccess?: () => void | Promise<void>;
|
|
23
37
|
trigger?: ReactNode;
|
|
@@ -30,6 +44,7 @@ export type PriceEditorProps = {
|
|
|
30
44
|
function PriceEditorInternal({
|
|
31
45
|
productId,
|
|
32
46
|
price,
|
|
47
|
+
seed,
|
|
33
48
|
propagateChanges,
|
|
34
49
|
onSuccess,
|
|
35
50
|
trigger,
|
|
@@ -42,6 +57,9 @@ function PriceEditorInternal({
|
|
|
42
57
|
const router = useI18nRouter();
|
|
43
58
|
const { createPrice, updatePrice, productId: contextProductId } = usePriceContext();
|
|
44
59
|
const [allFeatures, setAllFeatures] = useState<FeatureInterface[]>([]);
|
|
60
|
+
// Stays `!!price`, never `!!price || !!seed`: a seeded editor is a CREATE, so
|
|
61
|
+
// the create branch of onSubmit (fresh v4(), targetProductId, no id reuse)
|
|
62
|
+
// must run for it exactly as it does for an empty form.
|
|
45
63
|
const isEdit = !!price;
|
|
46
64
|
// NOT `price.productId` — that getter throws when the attribute is absent, and
|
|
47
65
|
// it always is (see the note in PriceContext). Only create mode needs this,
|
|
@@ -106,6 +124,24 @@ function PriceEditorInternal({
|
|
|
106
124
|
|
|
107
125
|
type PriceFormValues = z.infer<typeof formSchema>;
|
|
108
126
|
|
|
127
|
+
// Seeds carry platform features as NAMES, never ids: Feature ids differ per
|
|
128
|
+
// environment, names are the stable handle. A name this environment does not
|
|
129
|
+
// know is DROPPED rather than failing the whole seed — the effect below
|
|
130
|
+
// reports the dropped ones once.
|
|
131
|
+
const resolvePlatformFeatures = useCallback(
|
|
132
|
+
(names: string[] | undefined): { ids: string[]; unknown: string[] } => {
|
|
133
|
+
const ids: string[] = [];
|
|
134
|
+
const unknown: string[] = [];
|
|
135
|
+
for (const name of names ?? []) {
|
|
136
|
+
const match = allFeatures.find((feature) => feature.name === name);
|
|
137
|
+
if (match) ids.push(match.id);
|
|
138
|
+
else unknown.push(name);
|
|
139
|
+
}
|
|
140
|
+
return { ids, unknown };
|
|
141
|
+
},
|
|
142
|
+
[allFeatures],
|
|
143
|
+
);
|
|
144
|
+
|
|
109
145
|
// Fed to BOTH useForm and EditorSheet.onReset. The previous implementation
|
|
110
146
|
// reseeded with its own `useEffect(… form.reset)` on open; EditorSheet already
|
|
111
147
|
// owns that (EditorSheet.tsx:163-190) and running both fights over the form.
|
|
@@ -114,6 +150,30 @@ function PriceEditorInternal({
|
|
|
114
150
|
// (PriceArchiver), and a second toggle here would let the two disagree.
|
|
115
151
|
const getDefaultValues = useCallback((): PriceFormValues => {
|
|
116
152
|
const coreFeatureIds = allFeatures.filter((feature) => feature.isCore).map((feature) => feature.id);
|
|
153
|
+
|
|
154
|
+
// Seed branch: create mode only (`price` always wins — an edit form must
|
|
155
|
+
// show the price it edits, never a seed). Mirrors the `price` branch below
|
|
156
|
+
// field for field, including the /100: the seed keeps Stripe's MINOR units
|
|
157
|
+
// while the form field is in MAJOR units.
|
|
158
|
+
if (!price && seed) {
|
|
159
|
+
return {
|
|
160
|
+
unitAmount: seed.unitAmount / 100,
|
|
161
|
+
currency: seed.currency,
|
|
162
|
+
interval: seed.interval,
|
|
163
|
+
intervalCount: seed.intervalCount ?? 1,
|
|
164
|
+
usageType: seed.usageType ?? "licensed",
|
|
165
|
+
nickname: seed.nickname ?? "",
|
|
166
|
+
isTrial: seed.isTrial ?? false,
|
|
167
|
+
description: seed.description ?? "",
|
|
168
|
+
features: seed.features ?? [],
|
|
169
|
+
// `.toString()`, for the same reason the schema keeps the token
|
|
170
|
+
// pipeline string-based: a seeded `0` must survive as "0" and not be
|
|
171
|
+
// dropped as falsy.
|
|
172
|
+
token: seed.token?.toString() ?? "",
|
|
173
|
+
featureIds: [...new Set([...resolvePlatformFeatures(seed.platformFeatures).ids, ...coreFeatureIds])],
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
117
177
|
return {
|
|
118
178
|
unitAmount: price?.unitAmount ? price.unitAmount / 100 : 0,
|
|
119
179
|
currency: price?.currency ?? "usd",
|
|
@@ -127,13 +187,59 @@ function PriceEditorInternal({
|
|
|
127
187
|
token: price?.token?.toString() ?? "",
|
|
128
188
|
featureIds: [...new Set([...(price?.priceFeatures?.map((f) => f.id) ?? []), ...coreFeatureIds])],
|
|
129
189
|
};
|
|
130
|
-
}, [price, allFeatures]);
|
|
190
|
+
}, [price, seed, allFeatures, resolvePlatformFeatures]);
|
|
131
191
|
|
|
132
192
|
const form = useForm<PriceFormValues>({
|
|
133
193
|
resolver: zodResolver(formSchema) as any,
|
|
134
194
|
defaultValues: getDefaultValues(),
|
|
135
195
|
});
|
|
136
196
|
|
|
197
|
+
// ── The seeding race ──────────────────────────────────────────────────────
|
|
198
|
+
// `getDefaultValues` reads `allFeatures`, which the fetch effect above
|
|
199
|
+
// resolves asynchronously — so `useForm` has ALREADY seeded from an empty
|
|
200
|
+
// list by the time the features land, and every id-bearing default
|
|
201
|
+
// (`featureIds`: the core features, plus whatever the seed named) is missing.
|
|
202
|
+
//
|
|
203
|
+
// Nothing else fixes it: EditorSheet re-seeds on open only when `isEdit`
|
|
204
|
+
// (EditorSheet.tsx:209 — create defaults usually contain a fresh uuid, so an
|
|
205
|
+
// unconditional reset there would fire on every open and clobber parents that
|
|
206
|
+
// pre-fill via form.setValue). That is exactly why the core-feature
|
|
207
|
+
// checkboxes come up empty on a first create today.
|
|
208
|
+
//
|
|
209
|
+
// So catch up here, once, and only when it is safe:
|
|
210
|
+
// - `!isEdit` — the edit path is EditorSheet's, do not fight it.
|
|
211
|
+
// - `length > 0` — this is the empty → loaded transition, not the initial
|
|
212
|
+
// render (and not a genuinely empty feature list).
|
|
213
|
+
// - the ref — one shot per mount, so a later `allFeatures` identity
|
|
214
|
+
// change cannot re-reset a form the user is using.
|
|
215
|
+
// - `!isDirty` — never clobber anything already typed; a user who edited
|
|
216
|
+
// before the fetch returned keeps their input, and the
|
|
217
|
+
// one-shot ref is burned either way.
|
|
218
|
+
const hasCaughtUpWithFeatures = useRef(false);
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
if (isEdit || hasCaughtUpWithFeatures.current || allFeatures.length === 0) return;
|
|
221
|
+
hasCaughtUpWithFeatures.current = true;
|
|
222
|
+
if (form.formState.isDirty) return;
|
|
223
|
+
form.reset(getDefaultValues());
|
|
224
|
+
}, [allFeatures, isEdit, form, getDefaultValues]);
|
|
225
|
+
|
|
226
|
+
// Unknown platform feature names are reported ONCE per seed, not per render:
|
|
227
|
+
// `seed` typically arrives as an object literal from the list row or the
|
|
228
|
+
// import dialog, so its identity changes on every parent render — the guard
|
|
229
|
+
// keys on the names themselves instead. Waits for `allFeatures`, since before
|
|
230
|
+
// the fetch resolves every name looks unknown.
|
|
231
|
+
const warnedUnknownFor = useRef<string | undefined>(undefined);
|
|
232
|
+
useEffect(() => {
|
|
233
|
+
if (isEdit || !seed || allFeatures.length === 0) return;
|
|
234
|
+
const fingerprint = JSON.stringify(seed.platformFeatures ?? []);
|
|
235
|
+
if (warnedUnknownFor.current === fingerprint) return;
|
|
236
|
+
warnedUnknownFor.current = fingerprint;
|
|
237
|
+
|
|
238
|
+
const { unknown } = resolvePlatformFeatures(seed.platformFeatures);
|
|
239
|
+
if (unknown.length > 0)
|
|
240
|
+
showToast(t("billing.admin.prices.import.warnings.unknownFeatures", { features: unknown.join(", ") }));
|
|
241
|
+
}, [seed, isEdit, allFeatures, resolvePlatformFeatures, t]);
|
|
242
|
+
|
|
137
243
|
const watchInterval = form.watch("interval");
|
|
138
244
|
const isRecurring = watchInterval !== "one_time";
|
|
139
245
|
|