@carlonicora/nextjs-jsonapi 1.79.0 → 1.81.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 (84) hide show
  1. package/dist/{AssistantMessageInterface-DWnbd6J7.d.ts → AssistantMessageInterface-BpEhx2pC.d.ts} +18 -1
  2. package/dist/{AssistantMessageInterface-Mla6kgPe.d.mts → AssistantMessageInterface-DJ3Me16Y.d.mts} +18 -1
  3. package/dist/{BlockNoteEditor-6CBDTVKV.mjs → BlockNoteEditor-DCQA2PNW.mjs} +4 -4
  4. package/dist/{BlockNoteEditor-EH4HWI7H.js → BlockNoteEditor-ZISJ4KYX.js} +14 -14
  5. package/dist/{BlockNoteEditor-EH4HWI7H.js.map → BlockNoteEditor-ZISJ4KYX.js.map} +1 -1
  6. package/dist/billing/index.js +346 -346
  7. package/dist/billing/index.mjs +3 -3
  8. package/dist/{chunk-BKM5U3DE.mjs → chunk-6UMB5LTQ.mjs} +98 -7
  9. package/dist/chunk-6UMB5LTQ.mjs.map +1 -0
  10. package/dist/{chunk-ENRSFVOS.mjs → chunk-FZFJLDJY.mjs} +1290 -701
  11. package/dist/chunk-FZFJLDJY.mjs.map +1 -0
  12. package/dist/{chunk-5IEWLLLD.js → chunk-N4YZ45SK.js} +115 -24
  13. package/dist/chunk-N4YZ45SK.js.map +1 -0
  14. package/dist/{chunk-MEWXQEVE.mjs → chunk-PV5V6CVW.mjs} +2 -2
  15. package/dist/{chunk-ZDP3MBUI.js → chunk-TZJFHXDU.js} +1329 -740
  16. package/dist/chunk-TZJFHXDU.js.map +1 -0
  17. package/dist/{chunk-TWDSDTHU.js → chunk-ZEJSPTHS.js} +7 -7
  18. package/dist/{chunk-TWDSDTHU.js.map → chunk-ZEJSPTHS.js.map} +1 -1
  19. package/dist/client/index.js +4 -4
  20. package/dist/client/index.mjs +3 -3
  21. package/dist/components/index.d.mts +27 -7
  22. package/dist/components/index.d.ts +27 -7
  23. package/dist/components/index.js +8 -4
  24. package/dist/components/index.js.map +1 -1
  25. package/dist/components/index.mjs +7 -3
  26. package/dist/contexts/index.d.mts +1 -1
  27. package/dist/contexts/index.d.ts +1 -1
  28. package/dist/contexts/index.js +4 -4
  29. package/dist/contexts/index.mjs +3 -3
  30. package/dist/core/index.d.mts +35 -3
  31. package/dist/core/index.d.ts +35 -3
  32. package/dist/core/index.js +6 -2
  33. package/dist/core/index.js.map +1 -1
  34. package/dist/core/index.mjs +5 -1
  35. package/dist/index.d.mts +2 -2
  36. package/dist/index.d.ts +2 -2
  37. package/dist/index.js +7 -3
  38. package/dist/index.js.map +1 -1
  39. package/dist/index.mjs +6 -2
  40. package/dist/server/index.js +3 -3
  41. package/dist/server/index.mjs +1 -1
  42. package/package.json +1 -1
  43. package/src/components/index.ts +1 -0
  44. package/src/core/index.ts +2 -0
  45. package/src/core/registry/ModuleRegistry.ts +1 -0
  46. package/src/features/assistant/components/parts/AssistantThread.tsx +1 -1
  47. package/src/features/assistant-message/AssistantMessageModule.ts +4 -0
  48. package/src/features/assistant-message/components/MessageItem.tsx +7 -7
  49. package/src/features/assistant-message/components/MessageList.tsx +1 -1
  50. package/src/features/assistant-message/components/__tests__/MessageItem.spec.tsx +11 -7
  51. package/src/features/assistant-message/components/index.ts +1 -0
  52. package/src/features/assistant-message/components/parts/MessageSourcesContainer.tsx +135 -0
  53. package/src/features/assistant-message/components/parts/MessageSourcesPanel.tsx +151 -0
  54. package/src/features/assistant-message/components/parts/RelevanceMeter.tsx +29 -0
  55. package/src/features/assistant-message/components/parts/__tests__/MessageSourcesPanel.spec.tsx +70 -0
  56. package/src/features/assistant-message/components/parts/tabs/CitationsTab.tsx +105 -0
  57. package/src/features/assistant-message/components/parts/tabs/ContentsTab.tsx +88 -0
  58. package/src/features/assistant-message/components/parts/tabs/ReferencesTab.tsx +51 -0
  59. package/src/features/assistant-message/components/parts/tabs/SuggestedQuestionsTab.tsx +24 -0
  60. package/src/features/assistant-message/components/parts/tabs/UsersTab.tsx +142 -0
  61. package/src/features/assistant-message/data/AssistantMessage.ts +20 -0
  62. package/src/features/assistant-message/data/AssistantMessageInterface.ts +2 -0
  63. package/src/features/assistant-message/data/AssistantMessageService.ts +13 -4
  64. package/src/features/assistant-message/data/__tests__/AssistantMessage.citations.spec.ts +65 -0
  65. package/src/features/assistant-message/data/__tests__/AssistantMessage.spec.ts +8 -0
  66. package/src/features/chunk/ChunkModule.ts +18 -0
  67. package/src/features/chunk/data/Chunk.ts +49 -0
  68. package/src/features/chunk/data/ChunkInput.ts +3 -0
  69. package/src/features/chunk/data/ChunkInterface.ts +18 -0
  70. package/src/features/chunk/data/__tests__/Chunk.spec.ts +83 -0
  71. package/src/features/chunk/data/index.ts +3 -0
  72. package/src/features/chunk/index.ts +2 -0
  73. package/src/features/rbac/components/RbacByRoleContainer.tsx +270 -0
  74. package/src/features/rbac/index.ts +1 -0
  75. package/dist/chunk-5IEWLLLD.js.map +0 -1
  76. package/dist/chunk-BKM5U3DE.mjs.map +0 -1
  77. package/dist/chunk-ENRSFVOS.mjs.map +0 -1
  78. package/dist/chunk-ZDP3MBUI.js.map +0 -1
  79. package/src/features/assistant-message/components/parts/ReferenceBadges.tsx +0 -46
  80. package/src/features/assistant-message/components/parts/SuggestedFollowUps.tsx +0 -52
  81. package/src/features/assistant-message/components/parts/__tests__/ReferenceBadges.spec.tsx +0 -59
  82. package/src/features/assistant-message/components/parts/__tests__/SuggestedFollowUps.spec.tsx +0 -29
  83. /package/dist/{BlockNoteEditor-6CBDTVKV.mjs.map → BlockNoteEditor-DCQA2PNW.mjs.map} +0 -0
  84. /package/dist/{chunk-MEWXQEVE.mjs.map → chunk-PV5V6CVW.mjs.map} +0 -0
@@ -0,0 +1,105 @@
1
+ "use client";
2
+
3
+ import { Fragment, useState } from "react";
4
+ import { useTranslations } from "next-intl";
5
+ import { ChevronDown, HelpCircle } from "lucide-react";
6
+ import ReactMarkdown from "react-markdown";
7
+ import remarkGfm from "remark-gfm";
8
+ import type { ApiDataInterface } from "../../../../../core";
9
+ import type { ChunkInterface, ChunkRelationshipMeta } from "../../../../chunk/data/ChunkInterface";
10
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../../../../../shadcnui/ui/table";
11
+ import { Tooltip, TooltipContent, TooltipTrigger } from "../../../../../shadcnui/ui/tooltip";
12
+ import { cn } from "@/lib/utils";
13
+ import { RelevanceMeter } from "../RelevanceMeter";
14
+
15
+ interface Props {
16
+ citations: (ChunkInterface & ChunkRelationshipMeta)[];
17
+ /**
18
+ * Resolved source entities keyed by `chunk.nodeId`. When provided, the row
19
+ * label is the entity's `name`; otherwise it falls back to the nodeType + a
20
+ * short id.
21
+ */
22
+ sources?: Map<string, ApiDataInterface>;
23
+ }
24
+
25
+ export function CitationsTab({ citations, sources }: Props) {
26
+ const t = useTranslations();
27
+ const [expanded, setExpanded] = useState<Set<string>>(new Set());
28
+ if (citations.length === 0) return null;
29
+
30
+ const sorted = [...citations].sort((a, b) => (b.relevance ?? 0) - (a.relevance ?? 0));
31
+
32
+ const toggle = (id: string) => {
33
+ setExpanded((prev) => {
34
+ const next = new Set(prev);
35
+ if (next.has(id)) next.delete(id);
36
+ else next.add(id);
37
+ return next;
38
+ });
39
+ };
40
+
41
+ return (
42
+ <Table className="table-fixed">
43
+ <TableHeader>
44
+ <TableRow>
45
+ <TableHead>{t("features.assistant.message.sources.source")}</TableHead>
46
+ <TableHead className="w-28 text-center">{t("features.assistant.message.sources.relevance")}</TableHead>
47
+ </TableRow>
48
+ </TableHeader>
49
+ <TableBody>
50
+ {sorted.map((chunk) => {
51
+ const isOpen = expanded.has(chunk.id);
52
+ const resolved = chunk.nodeId ? sources?.get(chunk.nodeId) : undefined;
53
+ const fallbackName = chunk.nodeId
54
+ ? `${chunk.nodeType ?? t("features.assistant.message.sources.source")} ${chunk.nodeId.slice(0, 8)}`
55
+ : (chunk.nodeType ?? t("features.assistant.message.sources.source"));
56
+ const sourceName = (resolved as any)?.name ?? fallbackName;
57
+ return (
58
+ <Fragment key={chunk.id}>
59
+ <TableRow>
60
+ <TableCell>
61
+ <div
62
+ role="button"
63
+ tabIndex={0}
64
+ onClick={() => toggle(chunk.id)}
65
+ onKeyDown={(e) => {
66
+ if (e.key === "Enter" || e.key === " ") {
67
+ e.preventDefault();
68
+ toggle(chunk.id);
69
+ }
70
+ }}
71
+ aria-expanded={isOpen}
72
+ className="flex w-full cursor-pointer items-center justify-start gap-x-2"
73
+ >
74
+ <ChevronDown className={cn("h-4 w-4 transition-transform", !isOpen && "-rotate-90")} />
75
+ <span className="font-semibold">{sourceName}</span>
76
+ {chunk.reason && (
77
+ <Tooltip>
78
+ <TooltipTrigger className="text-muted-foreground inline-flex">
79
+ <HelpCircle className="h-3.5 w-3.5" />
80
+ </TooltipTrigger>
81
+ <TooltipContent className="max-w-64 text-xs">{chunk.reason}</TooltipContent>
82
+ </Tooltip>
83
+ )}
84
+ </div>
85
+ </TableCell>
86
+ <TableCell className="text-center">
87
+ <RelevanceMeter value={chunk.relevance ?? 0} />
88
+ </TableCell>
89
+ </TableRow>
90
+ {isOpen && (
91
+ <TableRow>
92
+ <TableCell colSpan={2} className="border-t-0 p-4">
93
+ <div className="bg-card w-full max-w-full overflow-x-auto rounded border p-4 text-sm break-words">
94
+ <ReactMarkdown remarkPlugins={[remarkGfm]}>{chunk.content}</ReactMarkdown>
95
+ </div>
96
+ </TableCell>
97
+ </TableRow>
98
+ )}
99
+ </Fragment>
100
+ );
101
+ })}
102
+ </TableBody>
103
+ </Table>
104
+ );
105
+ }
@@ -0,0 +1,88 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { useTranslations } from "next-intl";
5
+ import type { ApiDataInterface } from "../../../../../core";
6
+ import { ModuleRegistry } from "../../../../../core/registry/ModuleRegistry";
7
+ import { usePageUrlGenerator } from "../../../../../hooks";
8
+ import type { ChunkInterface, ChunkRelationshipMeta } from "../../../../chunk/data/ChunkInterface";
9
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../../../../../shadcnui/ui/table";
10
+
11
+ interface Props {
12
+ citations: (ChunkInterface & ChunkRelationshipMeta)[];
13
+ /** Resolved source entities keyed by `chunk.nodeId`. */
14
+ sources?: Map<string, ApiDataInterface>;
15
+ }
16
+
17
+ interface ContentRow {
18
+ source: ApiDataInterface;
19
+ citationCount: number;
20
+ maxRelevance: number;
21
+ }
22
+
23
+ export function ContentsTab({ citations, sources }: Props) {
24
+ const t = useTranslations();
25
+ const generate = usePageUrlGenerator();
26
+
27
+ // Group citations by nodeId, then materialise rows from the resolved sources
28
+ // map. Chunks without a resolved source entity are skipped.
29
+ const map = new Map<string, ContentRow>();
30
+ for (const c of citations) {
31
+ const id = c.nodeId;
32
+ if (!id) continue;
33
+ const source = sources?.get(id);
34
+ if (!source) continue;
35
+ const existing = map.get(id);
36
+ if (existing) {
37
+ existing.citationCount++;
38
+ existing.maxRelevance = Math.max(existing.maxRelevance, c.relevance ?? 0);
39
+ } else {
40
+ map.set(id, { source, citationCount: 1, maxRelevance: c.relevance ?? 0 });
41
+ }
42
+ }
43
+
44
+ const rows = Array.from(map.values()).sort(
45
+ (a, b) =>
46
+ b.maxRelevance - a.maxRelevance || ((a.source as any).name ?? "").localeCompare((b.source as any).name ?? ""),
47
+ );
48
+
49
+ if (rows.length === 0) return null;
50
+
51
+ return (
52
+ <Table>
53
+ <TableHeader>
54
+ <TableRow>
55
+ <TableHead>{t("features.assistant.message.sources.source")}</TableHead>
56
+ <TableHead className="w-32">{t("features.assistant.message.sources.type")}</TableHead>
57
+ </TableRow>
58
+ </TableHeader>
59
+ <TableBody>
60
+ {rows.map(({ source, citationCount }) => {
61
+ let module;
62
+ try {
63
+ module = ModuleRegistry.findByName(source.type);
64
+ } catch {
65
+ return null;
66
+ }
67
+ const href = generate({ page: module, id: source.id });
68
+ const name = (source as any).name ?? source.identifier;
69
+ return (
70
+ <TableRow key={`${source.type}/${source.id}`}>
71
+ <TableCell>
72
+ <Link href={href} target="_blank" rel="noopener noreferrer" className="hover:underline">
73
+ <span className="font-medium">{name}</span>{" "}
74
+ <span className="text-muted-foreground text-xs">
75
+ {t("features.assistant.message.sources.citations_count", {
76
+ count: citationCount,
77
+ })}
78
+ </span>
79
+ </Link>
80
+ </TableCell>
81
+ <TableCell className="text-muted-foreground text-xs">{module.name}</TableCell>
82
+ </TableRow>
83
+ );
84
+ })}
85
+ </TableBody>
86
+ </Table>
87
+ );
88
+ }
@@ -0,0 +1,51 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { useTranslations } from "next-intl";
5
+ import type { ApiDataInterface } from "../../../../../core";
6
+ import { ModuleRegistry } from "../../../../../core/registry/ModuleRegistry";
7
+ import { usePageUrlGenerator } from "../../../../../hooks";
8
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../../../../../shadcnui/ui/table";
9
+
10
+ interface Props {
11
+ references: ApiDataInterface[];
12
+ }
13
+
14
+ export function ReferencesTab({ references }: Props) {
15
+ const t = useTranslations();
16
+ const generate = usePageUrlGenerator();
17
+
18
+ if (references.length === 0) return null;
19
+
20
+ return (
21
+ <Table>
22
+ <TableHeader>
23
+ <TableRow>
24
+ <TableHead>{t("features.assistant.message.sources.source")}</TableHead>
25
+ <TableHead className="w-32">{t("features.assistant.message.sources.type")}</TableHead>
26
+ </TableRow>
27
+ </TableHeader>
28
+ <TableBody>
29
+ {references.map((ref) => {
30
+ let module;
31
+ try {
32
+ module = ModuleRegistry.findByName(ref.type);
33
+ } catch {
34
+ return null;
35
+ }
36
+ const href = generate({ page: module, id: ref.id });
37
+ return (
38
+ <TableRow key={`${ref.type}/${ref.id}`}>
39
+ <TableCell>
40
+ <Link href={href} target="_blank" rel="noopener noreferrer" className="font-medium hover:underline">
41
+ {ref.identifier}
42
+ </Link>
43
+ </TableCell>
44
+ <TableCell className="text-muted-foreground text-xs">{module.name}</TableCell>
45
+ </TableRow>
46
+ );
47
+ })}
48
+ </TableBody>
49
+ </Table>
50
+ );
51
+ }
@@ -0,0 +1,24 @@
1
+ "use client";
2
+
3
+ interface Props {
4
+ questions: string[];
5
+ onSelect: (question: string) => void;
6
+ }
7
+
8
+ export function SuggestedQuestionsTab({ questions, onSelect }: Props) {
9
+ if (questions.length === 0) return null;
10
+ return (
11
+ <div className="flex flex-col gap-1">
12
+ {questions.map((q) => (
13
+ <button
14
+ key={q}
15
+ type="button"
16
+ onClick={() => onSelect(q)}
17
+ className="border-border bg-muted/30 hover:bg-muted rounded-md border px-3 py-1.5 text-left text-sm"
18
+ >
19
+ {q}
20
+ </button>
21
+ ))}
22
+ </div>
23
+ );
24
+ }
@@ -0,0 +1,142 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { useTranslations } from "next-intl";
5
+ import type { ApiDataInterface } from "../../../../../core";
6
+ import { ModuleRegistry } from "../../../../../core/registry/ModuleRegistry";
7
+ import { usePageUrlGenerator } from "../../../../../hooks";
8
+ import type { ChunkInterface, ChunkRelationshipMeta } from "../../../../chunk/data/ChunkInterface";
9
+ import { Avatar, AvatarFallback, AvatarImage } from "../../../../../shadcnui/ui/avatar";
10
+ import { Table, TableBody, TableCell, TableRow } from "../../../../../shadcnui/ui/table";
11
+
12
+ interface Props {
13
+ /** Pre-deduplicated list of authors derived from the resolved sources. */
14
+ users: ApiDataInterface[];
15
+ /**
16
+ * Optional citations + sources used to compute per-user content/citation
17
+ * counts. When omitted, only the user list is shown.
18
+ */
19
+ citations?: (ChunkInterface & ChunkRelationshipMeta)[];
20
+ sources?: Map<string, ApiDataInterface>;
21
+ }
22
+
23
+ interface UserRow {
24
+ user: ApiDataInterface;
25
+ contentCount: number;
26
+ citationCount: number;
27
+ }
28
+
29
+ function getInitials(name: string): string {
30
+ return name
31
+ .split(/\s+/)
32
+ .map((p) => p.charAt(0))
33
+ .filter(Boolean)
34
+ .slice(0, 2)
35
+ .join("")
36
+ .toUpperCase();
37
+ }
38
+
39
+ function readAuthor(source: ApiDataInterface): ApiDataInterface | undefined {
40
+ // The Content base class throws on missing author. Catch defensively so a
41
+ // partially-hydrated source never breaks the UI.
42
+ try {
43
+ const a = (source as any).author;
44
+ return a && a.id ? a : undefined;
45
+ } catch {
46
+ return undefined;
47
+ }
48
+ }
49
+
50
+ export function UsersTab({ users, citations, sources }: Props) {
51
+ const t = useTranslations();
52
+ const generate = usePageUrlGenerator();
53
+
54
+ // Build per-user counts. If we have citations + sources, compute real counts.
55
+ // Otherwise fall back to one row per user with no counts.
56
+ const userMap = new Map<string, UserRow>();
57
+ for (const u of users) {
58
+ if (!u?.id) continue;
59
+ userMap.set(u.id, { user: u, contentCount: 0, citationCount: 0 });
60
+ }
61
+
62
+ if (citations && sources) {
63
+ const sourceUserPairs = new Set<string>();
64
+ for (const c of citations) {
65
+ const id = c.nodeId;
66
+ if (!id) continue;
67
+ const source = sources.get(id);
68
+ if (!source) continue;
69
+ const author = readAuthor(source);
70
+ if (!author) continue;
71
+
72
+ let row = userMap.get(author.id);
73
+ if (!row) {
74
+ row = { user: author, contentCount: 0, citationCount: 0 };
75
+ userMap.set(author.id, row);
76
+ }
77
+ const sourceUserKey = `${source.id}:${author.id}`;
78
+ if (!sourceUserPairs.has(sourceUserKey)) {
79
+ sourceUserPairs.add(sourceUserKey);
80
+ row.contentCount++;
81
+ }
82
+ row.citationCount++;
83
+ }
84
+ }
85
+
86
+ const rows = Array.from(userMap.values()).sort(
87
+ (a, b) =>
88
+ b.citationCount - a.citationCount || ((a.user as any).name ?? "").localeCompare((b.user as any).name ?? ""),
89
+ );
90
+
91
+ if (rows.length === 0) return null;
92
+
93
+ return (
94
+ <Table>
95
+ <TableBody>
96
+ {rows.map(({ user, contentCount, citationCount }) => {
97
+ let module;
98
+ try {
99
+ module = ModuleRegistry.findByName(user.type);
100
+ } catch {
101
+ return null;
102
+ }
103
+ const href = generate({ page: module, id: user.id });
104
+ const name = (user as any).name ?? (user as any).fullName ?? user.identifier ?? "User";
105
+ const avatarUrl = (user as any).avatar as string | undefined;
106
+ const showCounts = citationCount > 0 || contentCount > 0;
107
+ return (
108
+ <TableRow key={user.id}>
109
+ <TableCell>
110
+ <Link
111
+ href={href}
112
+ target="_blank"
113
+ rel="noopener noreferrer"
114
+ className="flex items-center gap-3 hover:underline"
115
+ >
116
+ <Avatar className="h-7 w-7">
117
+ <AvatarImage src={avatarUrl} aria-label={name} />
118
+ <AvatarFallback aria-label={name}>{getInitials(name)}</AvatarFallback>
119
+ </Avatar>
120
+ <span className="flex flex-col">
121
+ <span className="font-medium">{name}</span>
122
+ {showCounts && (
123
+ <span className="text-muted-foreground text-xs">
124
+ {t("features.assistant.message.sources.contents_count", {
125
+ count: contentCount,
126
+ })}
127
+ {" · "}
128
+ {t("features.assistant.message.sources.citations_count", {
129
+ count: citationCount,
130
+ })}
131
+ </span>
132
+ )}
133
+ </span>
134
+ </Link>
135
+ </TableCell>
136
+ </TableRow>
137
+ );
138
+ })}
139
+ </TableBody>
140
+ </Table>
141
+ );
142
+ }
@@ -1,6 +1,7 @@
1
1
  import { AbstractApiData, ApiDataInterface, JsonApiHydratedDataInterface, Modules } from "../../../core";
2
2
  import { AssistantMessageInput, AssistantMessageInterface, AssistantMessageRole } from "./AssistantMessageInterface";
3
3
  import { resolveReferenceableModules } from "../../assistant/utils/resolveReferenceableModules";
4
+ import { ChunkInterface, ChunkRelationshipMeta } from "../../chunk/data/ChunkInterface";
4
5
 
5
6
  export class AssistantMessage extends AbstractApiData implements AssistantMessageInterface {
6
7
  private _role?: AssistantMessageRole;
@@ -10,6 +11,7 @@ export class AssistantMessage extends AbstractApiData implements AssistantMessag
10
11
  private _inputTokens?: number;
11
12
  private _outputTokens?: number;
12
13
  private _references?: ApiDataInterface[];
14
+ private _citations?: (ChunkInterface & ChunkRelationshipMeta)[];
13
15
 
14
16
  get role(): AssistantMessageRole {
15
17
  return this._role ?? "assistant";
@@ -39,6 +41,10 @@ export class AssistantMessage extends AbstractApiData implements AssistantMessag
39
41
  return this._references ?? [];
40
42
  }
41
43
 
44
+ get citations(): (ChunkInterface & ChunkRelationshipMeta)[] {
45
+ return this._citations ?? [];
46
+ }
47
+
42
48
  rehydrate(data: JsonApiHydratedDataInterface): this {
43
49
  super.rehydrate(data);
44
50
  const attrs = data.jsonApi.attributes ?? {};
@@ -50,6 +56,20 @@ export class AssistantMessage extends AbstractApiData implements AssistantMessag
50
56
  this._outputTokens = attrs.outputTokens;
51
57
  const refs = this._readIncludedPolymorphic<ApiDataInterface>(data, "references", resolveReferenceableModules());
52
58
  this._references = Array.isArray(refs) ? refs : refs ? [refs] : [];
59
+ if (data.jsonApi.relationships?.citations?.data) {
60
+ const citations = this._readIncludedWithMeta<ChunkInterface, ChunkRelationshipMeta>(
61
+ data,
62
+ "citations",
63
+ Modules.Chunk,
64
+ );
65
+ this._citations = Array.isArray(citations)
66
+ ? (citations as (ChunkInterface & ChunkRelationshipMeta)[])
67
+ : citations
68
+ ? [citations as ChunkInterface & ChunkRelationshipMeta]
69
+ : [];
70
+ } else {
71
+ this._citations = [];
72
+ }
53
73
  return this;
54
74
  }
55
75
 
@@ -1,4 +1,5 @@
1
1
  import { ApiDataInterface } from "../../../core";
2
+ import { ChunkInterface, ChunkRelationshipMeta } from "../../chunk/data/ChunkInterface";
2
3
 
3
4
  export type AssistantMessageRole = "user" | "assistant" | "system";
4
5
 
@@ -18,4 +19,5 @@ export interface AssistantMessageInterface extends ApiDataInterface {
18
19
  get inputTokens(): number | undefined;
19
20
  get outputTokens(): number | undefined;
20
21
  get references(): ApiDataInterface[];
22
+ get citations(): (ChunkInterface & ChunkRelationshipMeta)[];
21
23
  }
@@ -8,6 +8,10 @@ export class AssistantMessageService extends AbstractService {
8
8
  id: params.assistantId,
9
9
  childEndpoint: Modules.AssistantMessage,
10
10
  });
11
+ if (Modules.AssistantMessage.inclusions?.lists?.fields)
12
+ endpoint.limitToFields(Modules.AssistantMessage.inclusions.lists.fields);
13
+ if (Modules.AssistantMessage.inclusions?.lists?.types)
14
+ endpoint.limitToType(Modules.AssistantMessage.inclusions.lists.types);
11
15
  return this.callApi({
12
16
  type: Modules.AssistantMessage,
13
17
  method: HttpMethod.GET,
@@ -17,13 +21,18 @@ export class AssistantMessageService extends AbstractService {
17
21
  }
18
22
 
19
23
  static async findOne(params: { id: string }): Promise<AssistantMessageInterface> {
24
+ const endpoint = new EndpointCreator({
25
+ endpoint: Modules.AssistantMessage,
26
+ id: params.id,
27
+ });
28
+ if (Modules.AssistantMessage.inclusions?.lists?.fields)
29
+ endpoint.limitToFields(Modules.AssistantMessage.inclusions.lists.fields);
30
+ if (Modules.AssistantMessage.inclusions?.lists?.types)
31
+ endpoint.limitToType(Modules.AssistantMessage.inclusions.lists.types);
20
32
  return this.callApi<AssistantMessageInterface>({
21
33
  type: Modules.AssistantMessage,
22
34
  method: HttpMethod.GET,
23
- endpoint: new EndpointCreator({
24
- endpoint: Modules.AssistantMessage,
25
- id: params.id,
26
- }).generate(),
35
+ endpoint: endpoint.generate(),
27
36
  });
28
37
  }
29
38
 
@@ -0,0 +1,65 @@
1
+ import { describe, expect, it, beforeAll, afterAll } from "vitest";
2
+ import { AssistantMessage } from "../AssistantMessage";
3
+ import { Chunk } from "../../../chunk/data/Chunk";
4
+ import { DataClassRegistry } from "../../../../core/registry/DataClassRegistry";
5
+ import { ModuleRegistry } from "../../../../core/registry/ModuleRegistry";
6
+ import { ApiRequestDataTypeInterface } from "../../../../core/interfaces/ApiRequestDataTypeInterface";
7
+
8
+ const chunkModule: ApiRequestDataTypeInterface = {
9
+ name: "chunks",
10
+ model: Chunk,
11
+ } as any;
12
+
13
+ beforeAll(() => {
14
+ DataClassRegistry.clear();
15
+ DataClassRegistry.registerObjectClass(chunkModule, Chunk);
16
+ ModuleRegistry.register("Chunk", chunkModule);
17
+ });
18
+
19
+ afterAll(() => {
20
+ DataClassRegistry.clear();
21
+ });
22
+
23
+ describe("AssistantMessage citations rehydration", () => {
24
+ it("rehydrates citations with edge meta (relevance, reason)", () => {
25
+ const msg = new AssistantMessage();
26
+ msg.rehydrate({
27
+ jsonApi: {
28
+ id: "m1",
29
+ type: "assistant-messages",
30
+ attributes: { role: "assistant", content: "answer", position: 1 },
31
+ relationships: {
32
+ citations: {
33
+ data: [{ type: "chunks", id: "c1", meta: { relevance: 0.9, reason: "primary source" } }],
34
+ },
35
+ },
36
+ } as any,
37
+ included: [
38
+ {
39
+ id: "c1",
40
+ type: "chunks",
41
+ attributes: { content: "evidence text" },
42
+ meta: { nodeId: "doc-1", nodeType: "documents" },
43
+ },
44
+ ],
45
+ });
46
+
47
+ expect(msg.citations).toHaveLength(1);
48
+ expect(msg.citations[0].content).toBe("evidence text");
49
+ expect((msg.citations[0] as any).relevance).toBe(0.9);
50
+ expect((msg.citations[0] as any).reason).toBe("primary source");
51
+ });
52
+
53
+ it("returns [] when citations relationship is absent", () => {
54
+ const msg = new AssistantMessage();
55
+ msg.rehydrate({
56
+ jsonApi: {
57
+ id: "m2",
58
+ type: "assistant-messages",
59
+ attributes: { role: "assistant", content: "x", position: 0 },
60
+ } as any,
61
+ included: [],
62
+ });
63
+ expect(msg.citations).toEqual([]);
64
+ });
65
+ });
@@ -5,6 +5,7 @@ import { ModuleRegistry } from "../../../../core/registry/ModuleRegistry";
5
5
  import { JsonApiHydratedDataInterface } from "../../../../core/interfaces/JsonApiHydratedDataInterface";
6
6
  import { ApiRequestDataTypeInterface } from "../../../../core/interfaces/ApiRequestDataTypeInterface";
7
7
  import { AssistantMessage } from "../AssistantMessage";
8
+ import { Chunk } from "../../../chunk/data/Chunk";
8
9
 
9
10
  // ---------------------------------------------------------------------------
10
11
  // Minimal test-only model — avoids collision with any real app module.
@@ -41,12 +42,19 @@ const assistantModule: ApiRequestDataTypeInterface = {
41
42
  model: class {},
42
43
  } as any;
43
44
 
45
+ const chunkModule: ApiRequestDataTypeInterface = {
46
+ name: "chunks",
47
+ model: Chunk,
48
+ } as any;
49
+
44
50
  beforeAll(() => {
45
51
  DataClassRegistry.clear();
46
52
  DataClassRegistry.registerObjectClass(testAccountModule, TestAccount);
53
+ DataClassRegistry.registerObjectClass(chunkModule, Chunk);
47
54
  ModuleRegistry.register("TestAccount", testAccountModule);
48
55
  ModuleRegistry.register("AssistantMessage", assistantMessageModule);
49
56
  ModuleRegistry.register("Assistant", assistantModule);
57
+ ModuleRegistry.register("Chunk", chunkModule);
50
58
  });
51
59
 
52
60
  afterAll(() => {
@@ -0,0 +1,18 @@
1
+ import { FileTextIcon } from "lucide-react";
2
+ import { createJsonApiInclusion } from "../../core";
3
+ import { ModuleFactory } from "../../permissions";
4
+ import { Chunk } from "./data/Chunk";
5
+
6
+ export const ChunkModule = (factory: ModuleFactory) =>
7
+ factory({
8
+ pageUrl: "/chunks",
9
+ name: "chunks",
10
+ model: Chunk,
11
+ moduleId: "9b1c2f64-7ea3-4df5-8b2a-91b6a8d0e3a4",
12
+ icon: FileTextIcon,
13
+ inclusions: {
14
+ lists: {
15
+ fields: [createJsonApiInclusion<Chunk>("chunks", ["content", "nodeId", "nodeType", "imagePath", "source"])],
16
+ },
17
+ },
18
+ });
@@ -0,0 +1,49 @@
1
+ import { AbstractApiData, ApiDataInterface, JsonApiHydratedDataInterface, Modules } from "../../../core";
2
+ import { resolveReferenceableModules } from "../../assistant/utils/resolveReferenceableModules";
3
+ import { ChunkInterface } from "./ChunkInterface";
4
+
5
+ export class Chunk extends AbstractApiData implements ChunkInterface {
6
+ private _content?: string;
7
+ private _nodeId?: string;
8
+ private _nodeType?: string;
9
+ private _imagePath?: string;
10
+ private _source?: ApiDataInterface;
11
+
12
+ get content(): string {
13
+ return this._content ?? "";
14
+ }
15
+ get nodeId(): string | undefined {
16
+ return this._nodeId;
17
+ }
18
+ get nodeType(): string | undefined {
19
+ return this._nodeType;
20
+ }
21
+ get imagePath(): string | undefined {
22
+ return this._imagePath;
23
+ }
24
+ get source(): ApiDataInterface | undefined {
25
+ return this._source;
26
+ }
27
+
28
+ rehydrate(data: JsonApiHydratedDataInterface): this {
29
+ super.rehydrate(data);
30
+ const attrs = data.jsonApi.attributes ?? {};
31
+ const meta = data.jsonApi.meta ?? {};
32
+ this._content = attrs.content;
33
+ this._nodeId = meta.nodeId ?? attrs.nodeId;
34
+ this._nodeType = meta.nodeType ?? attrs.nodeType;
35
+ this._imagePath = attrs.imagePath;
36
+
37
+ const src = this._readIncludedPolymorphic<ApiDataInterface>(data, "source", resolveReferenceableModules());
38
+ this._source = Array.isArray(src) ? src[0] : src;
39
+ return this;
40
+ }
41
+
42
+ createJsonApi() {
43
+ // Chunks are read-only from the assistant context. Provide a minimal stub.
44
+ return {
45
+ data: { type: Modules.Chunk.name, id: this.id, attributes: {}, relationships: {} },
46
+ included: [],
47
+ };
48
+ }
49
+ }
@@ -0,0 +1,3 @@
1
+ export type ChunkInput = {
2
+ id: string;
3
+ };