@carlonicora/nextjs-jsonapi 1.108.0 → 1.110.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-BYgI5z1-.d.mts → AssistantInterface-B1c8FhGA.d.mts} +2 -0
- package/dist/{AssistantInterface-DfDcz0gJ.d.ts → AssistantInterface-BBUHxOCd.d.ts} +2 -0
- package/dist/{AssistantMessageInterface-BpEhx2pC.d.ts → AssistantMessageInterface-BNzf1etU.d.ts} +16 -1
- package/dist/{AssistantMessageInterface-DJ3Me16Y.d.mts → AssistantMessageInterface-DrgHDjZ5.d.mts} +16 -1
- package/dist/{AuthComponent-B6DIk8Vf.d.ts → AuthComponent-Cd7lcYif.d.ts} +1 -1
- package/dist/{AuthComponent-BKI0ZbtD.d.mts → AuthComponent-DdxCFgUZ.d.mts} +1 -1
- package/dist/{BlockNoteEditor-2AXSTGGG.js → BlockNoteEditor-7OKW6HHN.js} +19 -19
- package/dist/{BlockNoteEditor-2AXSTGGG.js.map → BlockNoteEditor-7OKW6HHN.js.map} +1 -1
- package/dist/{BlockNoteEditor-XVIBGXHF.mjs → BlockNoteEditor-K7IIWM3S.mjs} +4 -4
- package/dist/BlockNoteViewer-OPSCTHKJ.mjs +31 -0
- package/dist/BlockNoteViewer-OPSCTHKJ.mjs.map +1 -0
- package/dist/BlockNoteViewer-UW5VZ5UF.js +31 -0
- package/dist/BlockNoteViewer-UW5VZ5UF.js.map +1 -0
- package/dist/{auth.interface-BBUgMZzs.d.ts → auth.interface-8b601idJ.d.ts} +1 -1
- package/dist/{auth.interface-XYEREOD6.d.mts → auth.interface-CXBF8Mhi.d.mts} +1 -1
- package/dist/billing/index.d.mts +41 -41
- package/dist/billing/index.d.ts +41 -41
- package/dist/billing/index.js +346 -346
- package/dist/billing/index.mjs +3 -3
- package/dist/{chunk-VLDLERJN.js → chunk-6XWW67F2.js} +29 -9
- package/dist/chunk-6XWW67F2.js.map +1 -0
- package/dist/{chunk-HC3JFN3C.js → chunk-AKXUNE3A.js} +1004 -862
- package/dist/chunk-AKXUNE3A.js.map +1 -0
- package/dist/{chunk-CFECWLHH.mjs → chunk-CWVYU4YZ.mjs} +23 -3
- package/dist/chunk-CWVYU4YZ.mjs.map +1 -0
- package/dist/{chunk-56XBGQGU.mjs → chunk-TXVVZLUQ.mjs} +1422 -1280
- package/dist/chunk-TXVVZLUQ.mjs.map +1 -0
- package/dist/{chunk-RXXZGPC3.js → chunk-U2HR57MY.js} +265 -113
- package/dist/chunk-U2HR57MY.js.map +1 -0
- package/dist/chunk-WPAQRB5Q.mjs +7 -0
- package/dist/chunk-WPAQRB5Q.mjs.map +1 -0
- package/dist/chunk-YBZVWLPH.js +7 -0
- package/dist/chunk-YBZVWLPH.js.map +1 -0
- package/dist/{chunk-N3NVIPSU.mjs → chunk-ZYPNVEHB.mjs} +235 -83
- package/dist/{chunk-N3NVIPSU.mjs.map → chunk-ZYPNVEHB.mjs.map} +1 -1
- package/dist/client/index.d.mts +9 -18
- package/dist/client/index.d.ts +9 -18
- package/dist/client/index.js +4 -4
- package/dist/client/index.mjs +3 -3
- package/dist/components/index.d.mts +421 -417
- package/dist/components/index.d.ts +421 -417
- package/dist/components/index.js +6 -4
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +5 -3
- package/dist/{config-CLQynoaa.d.ts → config-BrkU_Ynz.d.ts} +4 -1
- package/dist/{config-k61pe_o2.d.mts → config-snd2ToEB.d.mts} +4 -1
- package/dist/contexts/index.d.mts +31 -20
- package/dist/contexts/index.d.ts +31 -20
- package/dist/contexts/index.js +4 -4
- package/dist/contexts/index.mjs +3 -3
- package/dist/core/index.d.mts +75 -11
- package/dist/core/index.d.ts +75 -11
- package/dist/core/index.js +2 -2
- package/dist/core/index.mjs +1 -1
- package/dist/features/help/index.css +29 -0
- package/dist/features/help/index.css.map +1 -0
- package/dist/features/help/index.d.mts +74 -0
- package/dist/features/help/index.d.ts +74 -0
- package/dist/features/help/index.js +522 -0
- package/dist/features/help/index.js.map +1 -0
- package/dist/features/help/index.mjs +522 -0
- package/dist/features/help/index.mjs.map +1 -0
- package/dist/features/help/server.d.mts +33 -0
- package/dist/features/help/server.d.ts +33 -0
- package/dist/features/help/server.js +8 -0
- package/dist/features/help/server.js.map +1 -0
- package/dist/features/help/server.mjs +8 -0
- package/dist/features/help/server.mjs.map +1 -0
- package/dist/help-content-config.interface-C5ESIod9.d.mts +13 -0
- package/dist/help-content-config.interface-C5ESIod9.d.ts +13 -0
- package/dist/index.d.mts +10 -8
- package/dist/index.d.ts +10 -8
- package/dist/index.js +3 -3
- package/dist/index.mjs +2 -2
- package/dist/{notification.interface-aLEJbA_g.d.ts → notification.interface-C1T1C2ee.d.ts} +1 -100
- package/dist/{notification.interface-DLZGtV7Z.d.mts → notification.interface-DIxR23eS.d.mts} +1 -100
- package/dist/{s3.service-CVgLWaDc.d.mts → s3.service-0BTClOYO.d.mts} +2 -2
- package/dist/{s3.service-SLlX0Zbz.d.ts → s3.service-CT27Fm1s.d.ts} +2 -2
- package/dist/server/index.d.mts +4 -3
- package/dist/server/index.d.ts +4 -3
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +1 -1
- package/dist/testing/index.d.mts +1 -2
- package/dist/testing/index.d.ts +1 -2
- package/dist/types-CQSjy7et.d.mts +101 -0
- package/dist/types-DHOxe8rc.d.ts +101 -0
- package/dist/usePageUrlGenerator-tjq2mlDV.d.ts +14 -0
- package/dist/usePageUrlGenerator-uOnyJ6j2.d.mts +14 -0
- package/dist/{useSocket-BkxHHujj.d.mts → useSocket-B1fMIr17.d.mts} +1 -1
- package/dist/{useSocket-CMDjWFYm.d.ts → useSocket-BdJTBXKv.d.ts} +1 -1
- package/package.json +15 -1
- package/src/client/config.ts +9 -1
- package/src/components/index.ts +1 -0
- package/src/components/viewers/BlockNoteViewer.tsx +27 -0
- package/src/components/viewers/BlockNoteViewerContainer.tsx +10 -0
- package/src/core/registry/helpStore.ts +28 -0
- package/src/features/assistant/contexts/AssistantContext.tsx +35 -19
- package/src/features/assistant/contexts/__tests__/AssistantContext.spec.tsx +66 -6
- 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 +18 -8
- package/src/features/assistant-message/components/parts/MessageSourcesContainer.tsx +4 -0
- package/src/features/assistant-message/components/parts/MessageSourcesPanel.tsx +6 -4
- package/src/features/assistant-message/components/parts/tabs/ContentsTab.tsx +5 -1
- package/src/features/assistant-message/components/parts/tabs/ReferencesTab.tsx +3 -1
- package/src/features/assistant-message/data/AssistantMessage.ts +27 -1
- package/src/features/assistant-message/data/AssistantMessageInterface.ts +1 -0
- package/src/features/assistant-message/data/__tests__/AssistantMessage.spec.ts +7 -3
- package/src/features/help/components/HelpArticleBody.tsx +62 -0
- package/src/features/help/components/HelpAskAi.tsx +36 -0
- package/src/features/help/components/HelpAssistantSheet.tsx +53 -0
- package/src/features/help/components/HelpHeader.tsx +40 -0
- package/src/features/help/components/HelpHint.tsx +86 -0
- package/src/features/help/components/HelpSearchResultRow.tsx +61 -0
- package/src/features/help/components/HelpSideNav.tsx +119 -0
- package/src/features/help/components/HelpTOC.tsx +49 -0
- package/src/features/help/components/__tests__/HelpAskAi.spec.tsx +68 -0
- package/src/features/help/components/__tests__/HelpAssistantSheet.spec.tsx +36 -0
- package/src/features/help/components/__tests__/HelpHint.spec.tsx +50 -0
- package/src/features/help/components/__tests__/HelpSearchResultRow.spec.tsx +58 -0
- package/src/features/help/components/__tests__/HelpSideNav.spec.tsx +50 -0
- package/src/features/help/contexts/HelpContext.spec.tsx +26 -0
- package/src/features/help/contexts/HelpContext.tsx +24 -0
- package/src/features/help/hooks/useHelp.ts +1 -0
- package/src/features/help/hooks/useHelpFilter.ts +27 -0
- package/src/features/help/i18n-keys.ts +34 -0
- package/src/features/help/index.ts +24 -0
- package/src/features/help/interfaces/help-content-config.interface.ts +12 -0
- package/src/features/help/server-entry.ts +11 -0
- package/src/features/help/types/help-article.types.ts +35 -0
- package/src/features/help/utils/articleUrl.ts +13 -0
- package/src/features/how-to/HowToModule.ts +14 -1
- package/src/features/how-to/components/forms/HowToEditor.tsx +108 -4
- package/src/features/how-to/data/HowTo.spec.ts +43 -0
- package/src/features/how-to/data/HowTo.ts +64 -3
- package/src/features/how-to/data/HowToInterface.ts +14 -0
- package/src/features/how-to/data/HowToService.ts +61 -0
- package/src/index.ts +4 -0
- package/dist/chunk-56XBGQGU.mjs.map +0 -1
- package/dist/chunk-CFECWLHH.mjs.map +0 -1
- package/dist/chunk-HC3JFN3C.js.map +0 -1
- package/dist/chunk-RXXZGPC3.js.map +0 -1
- package/dist/chunk-VLDLERJN.js.map +0 -1
- /package/dist/{BlockNoteEditor-XVIBGXHF.mjs.map → BlockNoteEditor-K7IIWM3S.mjs.map} +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AbstractService, EndpointCreator, HttpMethod, Modules } from "../../../core";
|
|
2
|
-
import {
|
|
2
|
+
import { AssistantMessage } from "../../assistant-message/data/AssistantMessage";
|
|
3
3
|
import { AssistantMessageInterface } from "../../assistant-message/data/AssistantMessageInterface";
|
|
4
|
+
import { AssistantInput, AssistantInterface } from "./AssistantInterface";
|
|
4
5
|
|
|
5
6
|
export class AssistantService extends AbstractService {
|
|
6
7
|
static async findOne(params: { id: string }): Promise<AssistantInterface> {
|
|
@@ -33,8 +34,18 @@ export class AssistantService extends AbstractService {
|
|
|
33
34
|
/**
|
|
34
35
|
* Sends a new user message to an existing assistant thread. The agent turn
|
|
35
36
|
* runs synchronously; the response is a two-element list: [user, assistant].
|
|
37
|
+
*
|
|
38
|
+
* Uses the dedicated AssistantMessage.createAppendMessageJsonApi method to
|
|
39
|
+
* build the JSON:API envelope; this is the architecture-compliant pairing
|
|
40
|
+
* with `overridesJsonApiCreation: true`.
|
|
36
41
|
*/
|
|
37
|
-
static async appendMessage(params: {
|
|
42
|
+
static async appendMessage(params: {
|
|
43
|
+
assistantId: string;
|
|
44
|
+
content: string;
|
|
45
|
+
howToMode?: boolean;
|
|
46
|
+
limitToHowToId?: string;
|
|
47
|
+
}): Promise<AssistantMessageInterface[]> {
|
|
48
|
+
const message = new AssistantMessage();
|
|
38
49
|
return this.callApi<AssistantMessageInterface[]>({
|
|
39
50
|
type: Modules.AssistantMessage,
|
|
40
51
|
method: HttpMethod.POST,
|
|
@@ -43,12 +54,11 @@ export class AssistantService extends AbstractService {
|
|
|
43
54
|
id: params.assistantId,
|
|
44
55
|
childEndpoint: Modules.AssistantMessage,
|
|
45
56
|
}).generate(),
|
|
46
|
-
input: {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
},
|
|
57
|
+
input: message.createAppendMessageJsonApi({
|
|
58
|
+
content: params.content,
|
|
59
|
+
howToMode: params.howToMode,
|
|
60
|
+
limitToHowToId: params.limitToHowToId,
|
|
61
|
+
}),
|
|
52
62
|
overridesJsonApiCreation: true,
|
|
53
63
|
});
|
|
54
64
|
}
|
|
@@ -20,6 +20,10 @@ class SourcesFetcher extends ClientAbstractService {
|
|
|
20
20
|
}): Promise<ApiDataInterface[]> {
|
|
21
21
|
const endpoint = new EndpointCreator({ endpoint: params.module });
|
|
22
22
|
endpoint.addAdditionalParam(params.idsParam, params.ids.join(","));
|
|
23
|
+
// The list endpoint paginates and does not honour the `<x>Ids` filter, so a cited
|
|
24
|
+
// entity beyond the default first page never comes back and its citation falls back
|
|
25
|
+
// to "<Type> <id>". Request the full set and resolve client-side by id.
|
|
26
|
+
endpoint.addAdditionalParam("fetchAll", "true");
|
|
23
27
|
if (params.module.inclusions?.lists?.fields) endpoint.limitToFields(params.module.inclusions.lists.fields);
|
|
24
28
|
if (params.module.inclusions?.lists?.types) endpoint.limitToType(params.module.inclusions.lists.types);
|
|
25
29
|
return this.callApi<ApiDataInterface[]>({
|
|
@@ -55,12 +55,14 @@ export function MessageSourcesPanel({ message, isLatestAssistant, onSelectFollow
|
|
|
55
55
|
const suggestionsCount = isLatestAssistant ? message.suggestedQuestions.length : 0;
|
|
56
56
|
|
|
57
57
|
const contentsCount = useMemo(() => {
|
|
58
|
-
|
|
59
|
-
//
|
|
60
|
-
//
|
|
58
|
+
// Count unique nodeIds across citations. Mirrors what ContentsTab actually
|
|
59
|
+
// renders (one row per cited source), instead of `sources.size` which can
|
|
60
|
+
// include entities the fetch returned but no citation references.
|
|
61
61
|
const ids = new Set<string>();
|
|
62
62
|
for (const c of message.citations) {
|
|
63
|
-
if (c.nodeId)
|
|
63
|
+
if (!c.nodeId) continue;
|
|
64
|
+
if (sources && !sources.has(c.nodeId)) continue;
|
|
65
|
+
ids.add(c.nodeId);
|
|
64
66
|
}
|
|
65
67
|
return ids.size;
|
|
66
68
|
}, [message.citations, sources]);
|
|
@@ -64,7 +64,11 @@ export function ContentsTab({ citations, sources }: Props) {
|
|
|
64
64
|
} catch {
|
|
65
65
|
return null;
|
|
66
66
|
}
|
|
67
|
-
|
|
67
|
+
// Help HowTos are public articles, not admin records: route to
|
|
68
|
+
// /help/<mode>/<slug> instead of the module's /administration/howtos page.
|
|
69
|
+
const howToType = (source as any).howToType as string | undefined;
|
|
70
|
+
const slug = (source as any).slug as string | undefined;
|
|
71
|
+
const href = howToType && slug ? `/help/${howToType}/${slug}` : generate({ page: module, id: source.id });
|
|
68
72
|
const name = (source as any).name ?? source.identifier;
|
|
69
73
|
return (
|
|
70
74
|
<TableRow key={`${source.type}/${source.id}`}>
|
|
@@ -33,7 +33,9 @@ export function ReferencesTab({ references }: Props) {
|
|
|
33
33
|
} catch {
|
|
34
34
|
return null;
|
|
35
35
|
}
|
|
36
|
-
const
|
|
36
|
+
const howToType = (ref as any).howToType as string | undefined;
|
|
37
|
+
const slug = (ref as any).slug as string | undefined;
|
|
38
|
+
const href = howToType && slug ? `/help/${howToType}/${slug}` : generate({ page: module, id: ref.id });
|
|
37
39
|
return (
|
|
38
40
|
<TableRow key={`${ref.type}/${ref.id}`}>
|
|
39
41
|
<TableCell>
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { v4 as uuidv4 } from "uuid";
|
|
1
2
|
import { AbstractApiData, ApiDataInterface, JsonApiHydratedDataInterface, Modules } from "../../../core";
|
|
2
3
|
import { AssistantMessageInput, AssistantMessageInterface, AssistantMessageRole } from "./AssistantMessageInterface";
|
|
3
4
|
import { resolveReferenceableModules } from "../../assistant/utils/resolveReferenceableModules";
|
|
@@ -12,6 +13,7 @@ export class AssistantMessage extends AbstractApiData implements AssistantMessag
|
|
|
12
13
|
private _outputTokens?: number;
|
|
13
14
|
private _references?: ApiDataInterface[];
|
|
14
15
|
private _citations?: (ChunkInterface & ChunkRelationshipMeta)[];
|
|
16
|
+
private _isOptimistic = false;
|
|
15
17
|
|
|
16
18
|
get role(): AssistantMessageRole {
|
|
17
19
|
return this._role ?? "assistant";
|
|
@@ -45,6 +47,10 @@ export class AssistantMessage extends AbstractApiData implements AssistantMessag
|
|
|
45
47
|
return this._citations ?? [];
|
|
46
48
|
}
|
|
47
49
|
|
|
50
|
+
get isOptimistic(): boolean {
|
|
51
|
+
return this._isOptimistic;
|
|
52
|
+
}
|
|
53
|
+
|
|
48
54
|
rehydrate(data: JsonApiHydratedDataInterface): this {
|
|
49
55
|
super.rehydrate(data);
|
|
50
56
|
const attrs = data.jsonApi.attributes ?? {};
|
|
@@ -93,10 +99,29 @@ export class AssistantMessage extends AbstractApiData implements AssistantMessag
|
|
|
93
99
|
};
|
|
94
100
|
}
|
|
95
101
|
|
|
102
|
+
/**
|
|
103
|
+
* JSON:API envelope for POST /assistants/:id/assistant-messages.
|
|
104
|
+
* Different from `createJsonApi` (which expects a full message with role/position/assistant ref);
|
|
105
|
+
* the append-to-thread endpoint derives those server-side, so we only send `content` and
|
|
106
|
+
* the optional retrieval-mode flags.
|
|
107
|
+
*/
|
|
108
|
+
createAppendMessageJsonApi(params: { content: string; howToMode?: boolean; limitToHowToId?: string }) {
|
|
109
|
+
return {
|
|
110
|
+
data: {
|
|
111
|
+
type: Modules.AssistantMessage.name,
|
|
112
|
+
attributes: {
|
|
113
|
+
content: params.content,
|
|
114
|
+
...(params.howToMode !== undefined ? { howToMode: params.howToMode } : {}),
|
|
115
|
+
...(params.limitToHowToId !== undefined ? { limitToHowToId: params.limitToHowToId } : {}),
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
96
121
|
static buildOptimistic(params: { content: string; position: number; assistantId?: string }): AssistantMessage {
|
|
97
122
|
const msg = new AssistantMessage();
|
|
98
123
|
const jsonApi: Record<string, unknown> = {
|
|
99
|
-
id:
|
|
124
|
+
id: uuidv4(),
|
|
100
125
|
type: Modules.AssistantMessage.name,
|
|
101
126
|
attributes: {
|
|
102
127
|
role: "user",
|
|
@@ -110,6 +135,7 @@ export class AssistantMessage extends AbstractApiData implements AssistantMessag
|
|
|
110
135
|
};
|
|
111
136
|
}
|
|
112
137
|
msg.rehydrate({ jsonApi: jsonApi as any, included: [] });
|
|
138
|
+
msg._isOptimistic = true;
|
|
113
139
|
return msg;
|
|
114
140
|
}
|
|
115
141
|
}
|
|
@@ -136,14 +136,17 @@ describe("AssistantMessage.rehydrate", () => {
|
|
|
136
136
|
});
|
|
137
137
|
|
|
138
138
|
describe("AssistantMessage.buildOptimistic", () => {
|
|
139
|
-
|
|
139
|
+
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
140
|
+
|
|
141
|
+
it("creates a user message with a plain UUID id and the given content + position", () => {
|
|
140
142
|
const msg = AssistantMessage.buildOptimistic({
|
|
141
143
|
content: "hello",
|
|
142
144
|
assistantId: "a-1",
|
|
143
145
|
position: 3,
|
|
144
146
|
});
|
|
145
147
|
|
|
146
|
-
expect(msg.id
|
|
148
|
+
expect(msg.id).toMatch(UUID_RE);
|
|
149
|
+
expect(msg.isOptimistic).toBe(true);
|
|
147
150
|
expect(msg.role).toBe("user");
|
|
148
151
|
expect(msg.content).toBe("hello");
|
|
149
152
|
expect(msg.position).toBe(3);
|
|
@@ -152,7 +155,8 @@ describe("AssistantMessage.buildOptimistic", () => {
|
|
|
152
155
|
it("allows an omitted assistantId (first-message case)", () => {
|
|
153
156
|
const msg = AssistantMessage.buildOptimistic({ content: "first", position: 1 });
|
|
154
157
|
|
|
155
|
-
expect(msg.id
|
|
158
|
+
expect(msg.id).toMatch(UUID_RE);
|
|
159
|
+
expect(msg.isOptimistic).toBe(true);
|
|
156
160
|
expect(msg.role).toBe("user");
|
|
157
161
|
expect(msg.content).toBe("first");
|
|
158
162
|
expect(msg.position).toBe(1);
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ReactNode } from "react";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { useFormatter, useTranslations } from "next-intl";
|
|
6
|
+
import { usePageUrlGenerator } from "../../../client";
|
|
7
|
+
|
|
8
|
+
type Sibling = { howToType: string; slug: string; title: string };
|
|
9
|
+
|
|
10
|
+
export function HelpArticleBody(props: {
|
|
11
|
+
howToType: string;
|
|
12
|
+
title: string;
|
|
13
|
+
summary?: string;
|
|
14
|
+
updatedAt?: string;
|
|
15
|
+
prev?: Sibling | null;
|
|
16
|
+
next?: Sibling | null;
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
}) {
|
|
19
|
+
const t = useTranslations();
|
|
20
|
+
const format = useFormatter();
|
|
21
|
+
const generateUrl = usePageUrlGenerator();
|
|
22
|
+
const { howToType, title, summary, updatedAt, prev, next, children } = props;
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<article className="prose dark:prose-invert max-w-none">
|
|
26
|
+
<nav className="text-muted-foreground mb-2 text-xs">
|
|
27
|
+
<Link href={generateUrl({ page: "/help" })}>Help</Link>
|
|
28
|
+
{" · "}
|
|
29
|
+
<Link href={generateUrl({ page: `/help/${howToType}` })}>{t(`help.modes.${howToType}`)}</Link>
|
|
30
|
+
</nav>
|
|
31
|
+
<h1>{title}</h1>
|
|
32
|
+
{summary ? <p className="text-muted-foreground !mt-0 text-base">{summary}</p> : null}
|
|
33
|
+
{children}
|
|
34
|
+
<hr className="my-6" />
|
|
35
|
+
{updatedAt ? (
|
|
36
|
+
<div className="text-muted-foreground text-xs">
|
|
37
|
+
<span>
|
|
38
|
+
{t("help.article.lastUpdated", {
|
|
39
|
+
date: format.dateTime(new Date(updatedAt), { dateStyle: "short" }),
|
|
40
|
+
})}
|
|
41
|
+
</span>
|
|
42
|
+
</div>
|
|
43
|
+
) : null}
|
|
44
|
+
<div className="mt-4 flex justify-between text-sm">
|
|
45
|
+
{prev ? (
|
|
46
|
+
<Link href={generateUrl({ page: `/help/${prev.howToType}/${prev.slug}` })} className="hover:underline">
|
|
47
|
+
← {t("help.article.previous")}: {prev.title}
|
|
48
|
+
</Link>
|
|
49
|
+
) : (
|
|
50
|
+
<span />
|
|
51
|
+
)}
|
|
52
|
+
{next ? (
|
|
53
|
+
<Link href={generateUrl({ page: `/help/${next.howToType}/${next.slug}` })} className="hover:underline">
|
|
54
|
+
{t("help.article.next")}: {next.title} →
|
|
55
|
+
</Link>
|
|
56
|
+
) : (
|
|
57
|
+
<span />
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
</article>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { SparklesIcon } from "lucide-react";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
import { useCurrentUserContext } from "../../user/contexts/CurrentUserContext";
|
|
6
|
+
import { Button, Tooltip, TooltipTrigger, TooltipContent } from "../../../shadcnui";
|
|
7
|
+
import { HelpAssistantSheet } from "./HelpAssistantSheet";
|
|
8
|
+
|
|
9
|
+
export function HelpAskAi() {
|
|
10
|
+
const t = useTranslations();
|
|
11
|
+
const { currentUser } = useCurrentUserContext();
|
|
12
|
+
const [open, setOpen] = useState(false);
|
|
13
|
+
const disabled = !currentUser;
|
|
14
|
+
|
|
15
|
+
if (disabled) {
|
|
16
|
+
return (
|
|
17
|
+
<Tooltip>
|
|
18
|
+
<TooltipTrigger render={<Button variant="outline" size="sm" disabled />}>
|
|
19
|
+
<SparklesIcon className="h-4 w-4" />
|
|
20
|
+
{t("help.askAi.button")}
|
|
21
|
+
</TooltipTrigger>
|
|
22
|
+
<TooltipContent>{t("help.askAi.loginTooltip")}</TooltipContent>
|
|
23
|
+
</Tooltip>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<>
|
|
29
|
+
<Button variant="outline" size="sm" onClick={() => setOpen(true)}>
|
|
30
|
+
<SparklesIcon className="h-4 w-4" />
|
|
31
|
+
{t("help.askAi.button")}
|
|
32
|
+
</Button>
|
|
33
|
+
<HelpAssistantSheet open={open} onOpenChange={setOpen} />
|
|
34
|
+
</>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useTranslations } from "next-intl";
|
|
3
|
+
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from "../../../shadcnui";
|
|
4
|
+
import { AssistantProvider, useAssistantContext } from "../../assistant/contexts/AssistantContext";
|
|
5
|
+
import { AssistantThread } from "../../assistant/components/parts/AssistantThread";
|
|
6
|
+
import { AssistantComposer } from "../../assistant/components/parts/AssistantComposer";
|
|
7
|
+
import { AssistantEmptyState } from "../../assistant/components/parts/AssistantEmptyState";
|
|
8
|
+
|
|
9
|
+
export function HelpAssistantSheet({ open, onOpenChange }: { open: boolean; onOpenChange: (o: boolean) => void }) {
|
|
10
|
+
return (
|
|
11
|
+
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
12
|
+
<SheetContent
|
|
13
|
+
side="right"
|
|
14
|
+
className="flex w-full flex-col data-[side=right]:sm:max-w-2xl data-[side=right]:lg:max-w-3xl"
|
|
15
|
+
>
|
|
16
|
+
<AssistantProvider manageUrl={false}>
|
|
17
|
+
<HelpAssistantSheetBody />
|
|
18
|
+
</AssistantProvider>
|
|
19
|
+
</SheetContent>
|
|
20
|
+
</Sheet>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function HelpAssistantSheetBody() {
|
|
25
|
+
const t = useTranslations();
|
|
26
|
+
const ctx = useAssistantContext();
|
|
27
|
+
const showThread = !!ctx.assistant || ctx.sending || ctx.messages.length > 0;
|
|
28
|
+
const send = (content: string) => ctx.sendMessage(content, { howToMode: true });
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<>
|
|
32
|
+
<SheetHeader>
|
|
33
|
+
<SheetTitle>{t("help.askAi.sheet.title")}</SheetTitle>
|
|
34
|
+
<SheetDescription>{t("help.askAi.sheet.subtitle")}</SheetDescription>
|
|
35
|
+
</SheetHeader>
|
|
36
|
+
{!showThread ? (
|
|
37
|
+
<AssistantEmptyState onSend={send} />
|
|
38
|
+
) : (
|
|
39
|
+
<>
|
|
40
|
+
<AssistantThread
|
|
41
|
+
messages={ctx.messages}
|
|
42
|
+
sending={ctx.sending}
|
|
43
|
+
status={ctx.status}
|
|
44
|
+
onSelectFollowUp={send}
|
|
45
|
+
failedMessageIds={ctx.failedMessageIds}
|
|
46
|
+
onRetry={ctx.retrySend}
|
|
47
|
+
/>
|
|
48
|
+
<AssistantComposer onSend={send} disabled={ctx.sending} />
|
|
49
|
+
</>
|
|
50
|
+
)}
|
|
51
|
+
</>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import Image from "next/image";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
import { useCurrentUserContext } from "../../user/contexts/CurrentUserContext";
|
|
6
|
+
import { Button } from "../../../shadcnui";
|
|
7
|
+
import { useHelp } from "../contexts/HelpContext";
|
|
8
|
+
import { HelpAskAi } from "./HelpAskAi";
|
|
9
|
+
|
|
10
|
+
export function HelpHeader() {
|
|
11
|
+
const t = useTranslations();
|
|
12
|
+
const { currentUser } = useCurrentUserContext();
|
|
13
|
+
const { brand } = useHelp();
|
|
14
|
+
const logo = brand?.logo;
|
|
15
|
+
const label = brand?.label ?? "Help";
|
|
16
|
+
const appHref = brand?.appHref ?? "/";
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<header className="border-border bg-background flex items-center justify-between border-b px-4 py-3">
|
|
20
|
+
<Link href="/help" className="flex items-center gap-2">
|
|
21
|
+
{logo ? <Image src={logo} alt={label} width={28} height={28} /> : null}
|
|
22
|
+
<span className="text-base font-semibold">
|
|
23
|
+
{label} · {t("help.footerLink")}
|
|
24
|
+
</span>
|
|
25
|
+
</Link>
|
|
26
|
+
<div className="flex items-center gap-2">
|
|
27
|
+
<HelpAskAi />
|
|
28
|
+
{currentUser ? (
|
|
29
|
+
<Button render={<Link href={appHref} />} nativeButton={false} variant="outline" size="sm">
|
|
30
|
+
{t("help.header.openApp")}
|
|
31
|
+
</Button>
|
|
32
|
+
) : (
|
|
33
|
+
<Button render={<Link href="/login" />} nativeButton={false} variant="outline" size="sm">
|
|
34
|
+
{t("help.header.login")}
|
|
35
|
+
</Button>
|
|
36
|
+
)}
|
|
37
|
+
</div>
|
|
38
|
+
</header>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
import { HelpCircleIcon } from "lucide-react";
|
|
6
|
+
import {
|
|
7
|
+
Button,
|
|
8
|
+
Sheet,
|
|
9
|
+
SheetContent,
|
|
10
|
+
SheetTrigger,
|
|
11
|
+
SheetHeader,
|
|
12
|
+
SheetTitle,
|
|
13
|
+
SheetDescription,
|
|
14
|
+
} from "../../../shadcnui";
|
|
15
|
+
import { usePageUrlGenerator } from "../../../client";
|
|
16
|
+
import { HowToService } from "../../how-to/data/HowToService";
|
|
17
|
+
import type { HowToInterface } from "../../how-to/data/HowToInterface";
|
|
18
|
+
|
|
19
|
+
export function HelpHint({ contextKey }: { contextKey: string }) {
|
|
20
|
+
const t = useTranslations();
|
|
21
|
+
const generateUrl = usePageUrlGenerator();
|
|
22
|
+
const [open, setOpen] = useState(false);
|
|
23
|
+
const [picked, setPicked] = useState<HowToInterface | null>(null);
|
|
24
|
+
|
|
25
|
+
const [articles, setArticles] = useState<HowToInterface[]>([]);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
let active = true;
|
|
28
|
+
HowToService.findPublished()
|
|
29
|
+
.then((list) => active && setArticles(list))
|
|
30
|
+
.catch(() => active && setArticles([]));
|
|
31
|
+
return () => {
|
|
32
|
+
active = false;
|
|
33
|
+
};
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
const matches = useMemo(
|
|
37
|
+
() => articles.filter((a) => a.contextualKeys.includes(contextKey) && !a.draft),
|
|
38
|
+
[articles, contextKey],
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
if (matches.length === 0) return null;
|
|
42
|
+
const active = picked ?? (matches.length === 1 ? matches[0] : null);
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<Sheet
|
|
46
|
+
open={open}
|
|
47
|
+
onOpenChange={(o) => {
|
|
48
|
+
setOpen(o);
|
|
49
|
+
if (!o) setPicked(null);
|
|
50
|
+
}}
|
|
51
|
+
>
|
|
52
|
+
<SheetTrigger render={<Button variant="ghost" size="icon-sm" aria-label={t("help.hint.trigger")} />}>
|
|
53
|
+
<HelpCircleIcon className="h-4 w-4" />
|
|
54
|
+
</SheetTrigger>
|
|
55
|
+
<SheetContent side="right" className="w-full max-w-md sm:max-w-lg">
|
|
56
|
+
<SheetHeader>
|
|
57
|
+
<SheetTitle>{active ? active.name : t("help.hint.pickArticle")}</SheetTitle>
|
|
58
|
+
<SheetDescription>{active?.summary ?? ""}</SheetDescription>
|
|
59
|
+
</SheetHeader>
|
|
60
|
+
{active ? (
|
|
61
|
+
<div className="mt-4 space-y-3">
|
|
62
|
+
<p className="text-muted-foreground text-sm">{active.summary}</p>
|
|
63
|
+
<Link href={generateUrl({ page: `/help/${active.howToType}/${active.slug}` })} className="text-sm">
|
|
64
|
+
{t("help.hint.viewArticle")}
|
|
65
|
+
</Link>
|
|
66
|
+
</div>
|
|
67
|
+
) : (
|
|
68
|
+
<ul className="mt-4 space-y-1">
|
|
69
|
+
{matches.map((a) => (
|
|
70
|
+
<li key={a.id}>
|
|
71
|
+
<button
|
|
72
|
+
type="button"
|
|
73
|
+
onClick={() => setPicked(a)}
|
|
74
|
+
className="hover:bg-muted block w-full rounded px-2 py-1 text-left text-sm"
|
|
75
|
+
>
|
|
76
|
+
<div className="font-medium">{a.name}</div>
|
|
77
|
+
<div className="text-muted-foreground text-xs">{a.summary}</div>
|
|
78
|
+
</button>
|
|
79
|
+
</li>
|
|
80
|
+
))}
|
|
81
|
+
</ul>
|
|
82
|
+
)}
|
|
83
|
+
</SheetContent>
|
|
84
|
+
</Sheet>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import Link from "next/link";
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
import { LifeBuoyIcon, ArrowUpRightIcon } from "lucide-react";
|
|
6
|
+
import { CommandItem } from "../../../shadcnui";
|
|
7
|
+
import { usePageUrlGenerator } from "../../../client";
|
|
8
|
+
import { HowToService } from "../../how-to/data/HowToService";
|
|
9
|
+
import type { HowToInterface } from "../../how-to/data/HowToInterface";
|
|
10
|
+
|
|
11
|
+
export interface HelpSearchResultRowProps {
|
|
12
|
+
result: { id: string; name: string; entityType: string };
|
|
13
|
+
onSelect?: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function HelpSearchResultRow({ result, onSelect }: HelpSearchResultRowProps) {
|
|
17
|
+
const t = useTranslations();
|
|
18
|
+
const generateUrl = usePageUrlGenerator();
|
|
19
|
+
const [article, setArticle] = useState<HowToInterface | null>(null);
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
let active = true;
|
|
22
|
+
HowToService.findOne({ id: result.id })
|
|
23
|
+
.then((a) => active && setArticle(a))
|
|
24
|
+
.catch(() => active && setArticle(null));
|
|
25
|
+
return () => {
|
|
26
|
+
active = false;
|
|
27
|
+
};
|
|
28
|
+
}, [result.id]);
|
|
29
|
+
|
|
30
|
+
if (!article) {
|
|
31
|
+
return (
|
|
32
|
+
<CommandItem
|
|
33
|
+
value={result.id}
|
|
34
|
+
disabled
|
|
35
|
+
className="text-muted-foreground cursor-not-allowed px-3 py-1.5 text-sm italic"
|
|
36
|
+
>
|
|
37
|
+
{t("help.search.removed")}
|
|
38
|
+
</CommandItem>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<CommandItem value={result.id} onSelect={onSelect} className="cursor-pointer p-0">
|
|
44
|
+
<Link
|
|
45
|
+
href={generateUrl({ page: `/help/${article.howToType}/${article.slug}` })}
|
|
46
|
+
className="hover:bg-muted flex w-full items-center gap-3 rounded px-3 py-2"
|
|
47
|
+
>
|
|
48
|
+
<div className="bg-muted text-muted-foreground flex h-10 w-10 shrink-0 items-center justify-center rounded-full">
|
|
49
|
+
<LifeBuoyIcon className="h-5 w-5" />
|
|
50
|
+
</div>
|
|
51
|
+
<div className="flex min-w-0 flex-1 flex-col">
|
|
52
|
+
<span className="text-foreground truncate text-sm font-semibold">{article.name}</span>
|
|
53
|
+
<span className="text-muted-foreground truncate text-xs">
|
|
54
|
+
{t(`help.modes.${article.howToType}`)} · {article.summary}
|
|
55
|
+
</span>
|
|
56
|
+
</div>
|
|
57
|
+
<ArrowUpRightIcon className="text-muted-foreground h-5 w-5 shrink-0" />
|
|
58
|
+
</Link>
|
|
59
|
+
</CommandItem>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useState } from "react";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
import Link from "next/link";
|
|
6
|
+
import { usePathname } from "next/navigation";
|
|
7
|
+
import { HELP_MODES } from "../types/help-article.types";
|
|
8
|
+
import { usePageUrlGenerator } from "../../../client";
|
|
9
|
+
import { useHelpFilter } from "../hooks/useHelpFilter";
|
|
10
|
+
import { HowToService } from "../../how-to/data/HowToService";
|
|
11
|
+
import type { HowToInterface } from "../../how-to/data/HowToInterface";
|
|
12
|
+
|
|
13
|
+
export function HelpSideNav() {
|
|
14
|
+
const t = useTranslations();
|
|
15
|
+
const generateUrl = usePageUrlGenerator();
|
|
16
|
+
const pathname = usePathname();
|
|
17
|
+
const [articles, setArticles] = useState<HowToInterface[]>([]);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
let active = true;
|
|
21
|
+
HowToService.findPublished()
|
|
22
|
+
.then((list) => active && setArticles(list))
|
|
23
|
+
.catch(() => active && setArticles([]));
|
|
24
|
+
return () => {
|
|
25
|
+
active = false;
|
|
26
|
+
};
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
// useHelpFilter indexes by title/summary/tags — map HowTo getters into the shape it expects.
|
|
30
|
+
const indexable = useMemo(
|
|
31
|
+
() =>
|
|
32
|
+
articles.map((a) => ({
|
|
33
|
+
id: a.id,
|
|
34
|
+
title: a.name,
|
|
35
|
+
summary: a.summary ?? "",
|
|
36
|
+
tags: a.tags,
|
|
37
|
+
mode: a.howToType ?? "",
|
|
38
|
+
slug: a.slug ?? "",
|
|
39
|
+
})),
|
|
40
|
+
[articles],
|
|
41
|
+
);
|
|
42
|
+
const { query, setQuery, filtered } = useHelpFilter(indexable as any);
|
|
43
|
+
const filtering = query.trim().length > 0;
|
|
44
|
+
|
|
45
|
+
const groups = useMemo(
|
|
46
|
+
() =>
|
|
47
|
+
HELP_MODES.map((mode) => ({
|
|
48
|
+
mode,
|
|
49
|
+
articles: articles
|
|
50
|
+
.filter((a) => a.howToType === mode && !a.draft)
|
|
51
|
+
.sort((x, y) => (x.order ?? 0) - (y.order ?? 0)),
|
|
52
|
+
})),
|
|
53
|
+
[articles],
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
const isActive = (mode: string, slug: string) => pathname.endsWith(`/help/${mode}/${slug}`);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<nav aria-label="Help navigation" className="flex h-full flex-col gap-3 overflow-y-auto p-4">
|
|
60
|
+
<input
|
|
61
|
+
type="search"
|
|
62
|
+
value={query}
|
|
63
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
64
|
+
placeholder={t("help.sideNav.filterPlaceholder")}
|
|
65
|
+
className="border-input bg-background w-full rounded-md border px-3 py-2 text-sm"
|
|
66
|
+
/>
|
|
67
|
+
{filtering ? (
|
|
68
|
+
(filtered as any[]).length === 0 ? (
|
|
69
|
+
<p className="text-muted-foreground text-sm">{t("help.sideNav.noMatches")}</p>
|
|
70
|
+
) : (
|
|
71
|
+
<ul className="space-y-1">
|
|
72
|
+
{(filtered as any[]).map((a) => (
|
|
73
|
+
<li key={a.id}>
|
|
74
|
+
<Link
|
|
75
|
+
href={generateUrl({ page: `/help/${a.mode}/${a.slug}` })}
|
|
76
|
+
className={
|
|
77
|
+
"hover:bg-muted block rounded px-2 py-1 text-sm " +
|
|
78
|
+
(isActive(a.mode, a.slug) ? "bg-muted font-medium" : "")
|
|
79
|
+
}
|
|
80
|
+
>
|
|
81
|
+
{a.title}
|
|
82
|
+
</Link>
|
|
83
|
+
</li>
|
|
84
|
+
))}
|
|
85
|
+
</ul>
|
|
86
|
+
)
|
|
87
|
+
) : (
|
|
88
|
+
groups.map((group) => {
|
|
89
|
+
if (group.articles.length === 0) return null;
|
|
90
|
+
return (
|
|
91
|
+
<div key={group.mode}>
|
|
92
|
+
<Link
|
|
93
|
+
href={generateUrl({ page: `/help/${group.mode}` })}
|
|
94
|
+
className="text-muted-foreground hover:text-foreground mb-1 block text-xs font-semibold tracking-wider uppercase"
|
|
95
|
+
>
|
|
96
|
+
{t(`help.modes.${group.mode}`)}
|
|
97
|
+
</Link>
|
|
98
|
+
<ul className="space-y-0.5">
|
|
99
|
+
{group.articles.map((a) => (
|
|
100
|
+
<li key={a.id}>
|
|
101
|
+
<Link
|
|
102
|
+
href={generateUrl({ page: `/help/${a.howToType}/${a.slug}` })}
|
|
103
|
+
className={
|
|
104
|
+
"hover:bg-muted block rounded px-2 py-1 text-sm " +
|
|
105
|
+
(isActive(a.howToType ?? "", a.slug ?? "") ? "bg-muted font-medium" : "")
|
|
106
|
+
}
|
|
107
|
+
>
|
|
108
|
+
{a.name}
|
|
109
|
+
</Link>
|
|
110
|
+
</li>
|
|
111
|
+
))}
|
|
112
|
+
</ul>
|
|
113
|
+
</div>
|
|
114
|
+
);
|
|
115
|
+
})
|
|
116
|
+
)}
|
|
117
|
+
</nav>
|
|
118
|
+
);
|
|
119
|
+
}
|