@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.
Files changed (144) hide show
  1. package/dist/{AssistantInterface-BYgI5z1-.d.mts → AssistantInterface-B1c8FhGA.d.mts} +2 -0
  2. package/dist/{AssistantInterface-DfDcz0gJ.d.ts → AssistantInterface-BBUHxOCd.d.ts} +2 -0
  3. package/dist/{AssistantMessageInterface-BpEhx2pC.d.ts → AssistantMessageInterface-BNzf1etU.d.ts} +16 -1
  4. package/dist/{AssistantMessageInterface-DJ3Me16Y.d.mts → AssistantMessageInterface-DrgHDjZ5.d.mts} +16 -1
  5. package/dist/{AuthComponent-B6DIk8Vf.d.ts → AuthComponent-Cd7lcYif.d.ts} +1 -1
  6. package/dist/{AuthComponent-BKI0ZbtD.d.mts → AuthComponent-DdxCFgUZ.d.mts} +1 -1
  7. package/dist/{BlockNoteEditor-2AXSTGGG.js → BlockNoteEditor-7OKW6HHN.js} +19 -19
  8. package/dist/{BlockNoteEditor-2AXSTGGG.js.map → BlockNoteEditor-7OKW6HHN.js.map} +1 -1
  9. package/dist/{BlockNoteEditor-XVIBGXHF.mjs → BlockNoteEditor-K7IIWM3S.mjs} +4 -4
  10. package/dist/BlockNoteViewer-OPSCTHKJ.mjs +31 -0
  11. package/dist/BlockNoteViewer-OPSCTHKJ.mjs.map +1 -0
  12. package/dist/BlockNoteViewer-UW5VZ5UF.js +31 -0
  13. package/dist/BlockNoteViewer-UW5VZ5UF.js.map +1 -0
  14. package/dist/{auth.interface-BBUgMZzs.d.ts → auth.interface-8b601idJ.d.ts} +1 -1
  15. package/dist/{auth.interface-XYEREOD6.d.mts → auth.interface-CXBF8Mhi.d.mts} +1 -1
  16. package/dist/billing/index.d.mts +41 -41
  17. package/dist/billing/index.d.ts +41 -41
  18. package/dist/billing/index.js +346 -346
  19. package/dist/billing/index.mjs +3 -3
  20. package/dist/{chunk-VLDLERJN.js → chunk-6XWW67F2.js} +29 -9
  21. package/dist/chunk-6XWW67F2.js.map +1 -0
  22. package/dist/{chunk-HC3JFN3C.js → chunk-AKXUNE3A.js} +1004 -862
  23. package/dist/chunk-AKXUNE3A.js.map +1 -0
  24. package/dist/{chunk-CFECWLHH.mjs → chunk-CWVYU4YZ.mjs} +23 -3
  25. package/dist/chunk-CWVYU4YZ.mjs.map +1 -0
  26. package/dist/{chunk-56XBGQGU.mjs → chunk-TXVVZLUQ.mjs} +1422 -1280
  27. package/dist/chunk-TXVVZLUQ.mjs.map +1 -0
  28. package/dist/{chunk-RXXZGPC3.js → chunk-U2HR57MY.js} +265 -113
  29. package/dist/chunk-U2HR57MY.js.map +1 -0
  30. package/dist/chunk-WPAQRB5Q.mjs +7 -0
  31. package/dist/chunk-WPAQRB5Q.mjs.map +1 -0
  32. package/dist/chunk-YBZVWLPH.js +7 -0
  33. package/dist/chunk-YBZVWLPH.js.map +1 -0
  34. package/dist/{chunk-N3NVIPSU.mjs → chunk-ZYPNVEHB.mjs} +235 -83
  35. package/dist/{chunk-N3NVIPSU.mjs.map → chunk-ZYPNVEHB.mjs.map} +1 -1
  36. package/dist/client/index.d.mts +9 -18
  37. package/dist/client/index.d.ts +9 -18
  38. package/dist/client/index.js +4 -4
  39. package/dist/client/index.mjs +3 -3
  40. package/dist/components/index.d.mts +421 -417
  41. package/dist/components/index.d.ts +421 -417
  42. package/dist/components/index.js +6 -4
  43. package/dist/components/index.js.map +1 -1
  44. package/dist/components/index.mjs +5 -3
  45. package/dist/{config-CLQynoaa.d.ts → config-BrkU_Ynz.d.ts} +4 -1
  46. package/dist/{config-k61pe_o2.d.mts → config-snd2ToEB.d.mts} +4 -1
  47. package/dist/contexts/index.d.mts +31 -20
  48. package/dist/contexts/index.d.ts +31 -20
  49. package/dist/contexts/index.js +4 -4
  50. package/dist/contexts/index.mjs +3 -3
  51. package/dist/core/index.d.mts +75 -11
  52. package/dist/core/index.d.ts +75 -11
  53. package/dist/core/index.js +2 -2
  54. package/dist/core/index.mjs +1 -1
  55. package/dist/features/help/index.css +29 -0
  56. package/dist/features/help/index.css.map +1 -0
  57. package/dist/features/help/index.d.mts +74 -0
  58. package/dist/features/help/index.d.ts +74 -0
  59. package/dist/features/help/index.js +522 -0
  60. package/dist/features/help/index.js.map +1 -0
  61. package/dist/features/help/index.mjs +522 -0
  62. package/dist/features/help/index.mjs.map +1 -0
  63. package/dist/features/help/server.d.mts +33 -0
  64. package/dist/features/help/server.d.ts +33 -0
  65. package/dist/features/help/server.js +8 -0
  66. package/dist/features/help/server.js.map +1 -0
  67. package/dist/features/help/server.mjs +8 -0
  68. package/dist/features/help/server.mjs.map +1 -0
  69. package/dist/help-content-config.interface-C5ESIod9.d.mts +13 -0
  70. package/dist/help-content-config.interface-C5ESIod9.d.ts +13 -0
  71. package/dist/index.d.mts +10 -8
  72. package/dist/index.d.ts +10 -8
  73. package/dist/index.js +3 -3
  74. package/dist/index.mjs +2 -2
  75. package/dist/{notification.interface-aLEJbA_g.d.ts → notification.interface-C1T1C2ee.d.ts} +1 -100
  76. package/dist/{notification.interface-DLZGtV7Z.d.mts → notification.interface-DIxR23eS.d.mts} +1 -100
  77. package/dist/{s3.service-CVgLWaDc.d.mts → s3.service-0BTClOYO.d.mts} +2 -2
  78. package/dist/{s3.service-SLlX0Zbz.d.ts → s3.service-CT27Fm1s.d.ts} +2 -2
  79. package/dist/server/index.d.mts +4 -3
  80. package/dist/server/index.d.ts +4 -3
  81. package/dist/server/index.js +3 -3
  82. package/dist/server/index.mjs +1 -1
  83. package/dist/testing/index.d.mts +1 -2
  84. package/dist/testing/index.d.ts +1 -2
  85. package/dist/types-CQSjy7et.d.mts +101 -0
  86. package/dist/types-DHOxe8rc.d.ts +101 -0
  87. package/dist/usePageUrlGenerator-tjq2mlDV.d.ts +14 -0
  88. package/dist/usePageUrlGenerator-uOnyJ6j2.d.mts +14 -0
  89. package/dist/{useSocket-BkxHHujj.d.mts → useSocket-B1fMIr17.d.mts} +1 -1
  90. package/dist/{useSocket-CMDjWFYm.d.ts → useSocket-BdJTBXKv.d.ts} +1 -1
  91. package/package.json +15 -1
  92. package/src/client/config.ts +9 -1
  93. package/src/components/index.ts +1 -0
  94. package/src/components/viewers/BlockNoteViewer.tsx +27 -0
  95. package/src/components/viewers/BlockNoteViewerContainer.tsx +10 -0
  96. package/src/core/registry/helpStore.ts +28 -0
  97. package/src/features/assistant/contexts/AssistantContext.tsx +35 -19
  98. package/src/features/assistant/contexts/__tests__/AssistantContext.spec.tsx +66 -6
  99. package/src/features/assistant/data/Assistant.ts +2 -0
  100. package/src/features/assistant/data/AssistantInterface.ts +2 -0
  101. package/src/features/assistant/data/AssistantService.ts +18 -8
  102. package/src/features/assistant-message/components/parts/MessageSourcesContainer.tsx +4 -0
  103. package/src/features/assistant-message/components/parts/MessageSourcesPanel.tsx +6 -4
  104. package/src/features/assistant-message/components/parts/tabs/ContentsTab.tsx +5 -1
  105. package/src/features/assistant-message/components/parts/tabs/ReferencesTab.tsx +3 -1
  106. package/src/features/assistant-message/data/AssistantMessage.ts +27 -1
  107. package/src/features/assistant-message/data/AssistantMessageInterface.ts +1 -0
  108. package/src/features/assistant-message/data/__tests__/AssistantMessage.spec.ts +7 -3
  109. package/src/features/help/components/HelpArticleBody.tsx +62 -0
  110. package/src/features/help/components/HelpAskAi.tsx +36 -0
  111. package/src/features/help/components/HelpAssistantSheet.tsx +53 -0
  112. package/src/features/help/components/HelpHeader.tsx +40 -0
  113. package/src/features/help/components/HelpHint.tsx +86 -0
  114. package/src/features/help/components/HelpSearchResultRow.tsx +61 -0
  115. package/src/features/help/components/HelpSideNav.tsx +119 -0
  116. package/src/features/help/components/HelpTOC.tsx +49 -0
  117. package/src/features/help/components/__tests__/HelpAskAi.spec.tsx +68 -0
  118. package/src/features/help/components/__tests__/HelpAssistantSheet.spec.tsx +36 -0
  119. package/src/features/help/components/__tests__/HelpHint.spec.tsx +50 -0
  120. package/src/features/help/components/__tests__/HelpSearchResultRow.spec.tsx +58 -0
  121. package/src/features/help/components/__tests__/HelpSideNav.spec.tsx +50 -0
  122. package/src/features/help/contexts/HelpContext.spec.tsx +26 -0
  123. package/src/features/help/contexts/HelpContext.tsx +24 -0
  124. package/src/features/help/hooks/useHelp.ts +1 -0
  125. package/src/features/help/hooks/useHelpFilter.ts +27 -0
  126. package/src/features/help/i18n-keys.ts +34 -0
  127. package/src/features/help/index.ts +24 -0
  128. package/src/features/help/interfaces/help-content-config.interface.ts +12 -0
  129. package/src/features/help/server-entry.ts +11 -0
  130. package/src/features/help/types/help-article.types.ts +35 -0
  131. package/src/features/help/utils/articleUrl.ts +13 -0
  132. package/src/features/how-to/HowToModule.ts +14 -1
  133. package/src/features/how-to/components/forms/HowToEditor.tsx +108 -4
  134. package/src/features/how-to/data/HowTo.spec.ts +43 -0
  135. package/src/features/how-to/data/HowTo.ts +64 -3
  136. package/src/features/how-to/data/HowToInterface.ts +14 -0
  137. package/src/features/how-to/data/HowToService.ts +61 -0
  138. package/src/index.ts +4 -0
  139. package/dist/chunk-56XBGQGU.mjs.map +0 -1
  140. package/dist/chunk-CFECWLHH.mjs.map +0 -1
  141. package/dist/chunk-HC3JFN3C.js.map +0 -1
  142. package/dist/chunk-RXXZGPC3.js.map +0 -1
  143. package/dist/chunk-VLDLERJN.js.map +0 -1
  144. /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 { AssistantInput, AssistantInterface } from "./AssistantInterface";
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: { assistantId: string; content: string }): Promise<AssistantMessageInterface[]> {
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
- data: {
48
- type: Modules.AssistantMessage.name,
49
- attributes: { content: params.content },
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
- if (sources) return sources.size;
59
- // Fallback: derive from unique nodeId on chunks when sources haven't been
60
- // supplied yet (e.g., during the initial fetch).
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) ids.add(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
- const href = generate({ page: module, id: source.id });
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 href = generate({ page: module, id: ref.id });
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: `tmp-${crypto.randomUUID()}`,
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
  }
@@ -20,4 +20,5 @@ export interface AssistantMessageInterface extends ApiDataInterface {
20
20
  get outputTokens(): number | undefined;
21
21
  get references(): ApiDataInterface[];
22
22
  get citations(): (ChunkInterface & ChunkRelationshipMeta)[];
23
+ get isOptimistic(): boolean;
23
24
  }
@@ -136,14 +136,17 @@ describe("AssistantMessage.rehydrate", () => {
136
136
  });
137
137
 
138
138
  describe("AssistantMessage.buildOptimistic", () => {
139
- it("creates a user message with a tmp-prefixed id and the given content + position", () => {
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.startsWith("tmp-")).toBe(true);
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.startsWith("tmp-")).toBe(true);
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
+ }