@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.
- package/dist/{AssistantMessageInterface-DWnbd6J7.d.ts → AssistantMessageInterface-BpEhx2pC.d.ts} +18 -1
- package/dist/{AssistantMessageInterface-Mla6kgPe.d.mts → AssistantMessageInterface-DJ3Me16Y.d.mts} +18 -1
- package/dist/{BlockNoteEditor-6CBDTVKV.mjs → BlockNoteEditor-DCQA2PNW.mjs} +4 -4
- package/dist/{BlockNoteEditor-EH4HWI7H.js → BlockNoteEditor-ZISJ4KYX.js} +14 -14
- package/dist/{BlockNoteEditor-EH4HWI7H.js.map → BlockNoteEditor-ZISJ4KYX.js.map} +1 -1
- package/dist/billing/index.js +346 -346
- package/dist/billing/index.mjs +3 -3
- package/dist/{chunk-BKM5U3DE.mjs → chunk-6UMB5LTQ.mjs} +98 -7
- package/dist/chunk-6UMB5LTQ.mjs.map +1 -0
- package/dist/{chunk-ENRSFVOS.mjs → chunk-FZFJLDJY.mjs} +1290 -701
- package/dist/chunk-FZFJLDJY.mjs.map +1 -0
- package/dist/{chunk-5IEWLLLD.js → chunk-N4YZ45SK.js} +115 -24
- package/dist/chunk-N4YZ45SK.js.map +1 -0
- package/dist/{chunk-MEWXQEVE.mjs → chunk-PV5V6CVW.mjs} +2 -2
- package/dist/{chunk-ZDP3MBUI.js → chunk-TZJFHXDU.js} +1329 -740
- package/dist/chunk-TZJFHXDU.js.map +1 -0
- package/dist/{chunk-TWDSDTHU.js → chunk-ZEJSPTHS.js} +7 -7
- package/dist/{chunk-TWDSDTHU.js.map → chunk-ZEJSPTHS.js.map} +1 -1
- package/dist/client/index.js +4 -4
- package/dist/client/index.mjs +3 -3
- package/dist/components/index.d.mts +27 -7
- package/dist/components/index.d.ts +27 -7
- package/dist/components/index.js +8 -4
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +7 -3
- package/dist/contexts/index.d.mts +1 -1
- package/dist/contexts/index.d.ts +1 -1
- package/dist/contexts/index.js +4 -4
- package/dist/contexts/index.mjs +3 -3
- package/dist/core/index.d.mts +35 -3
- package/dist/core/index.d.ts +35 -3
- package/dist/core/index.js +6 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +5 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +7 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6 -2
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +1 -1
- package/package.json +1 -1
- package/src/components/index.ts +1 -0
- package/src/core/index.ts +2 -0
- package/src/core/registry/ModuleRegistry.ts +1 -0
- package/src/features/assistant/components/parts/AssistantThread.tsx +1 -1
- package/src/features/assistant-message/AssistantMessageModule.ts +4 -0
- package/src/features/assistant-message/components/MessageItem.tsx +7 -7
- package/src/features/assistant-message/components/MessageList.tsx +1 -1
- package/src/features/assistant-message/components/__tests__/MessageItem.spec.tsx +11 -7
- package/src/features/assistant-message/components/index.ts +1 -0
- package/src/features/assistant-message/components/parts/MessageSourcesContainer.tsx +135 -0
- package/src/features/assistant-message/components/parts/MessageSourcesPanel.tsx +151 -0
- package/src/features/assistant-message/components/parts/RelevanceMeter.tsx +29 -0
- package/src/features/assistant-message/components/parts/__tests__/MessageSourcesPanel.spec.tsx +70 -0
- package/src/features/assistant-message/components/parts/tabs/CitationsTab.tsx +105 -0
- package/src/features/assistant-message/components/parts/tabs/ContentsTab.tsx +88 -0
- package/src/features/assistant-message/components/parts/tabs/ReferencesTab.tsx +51 -0
- package/src/features/assistant-message/components/parts/tabs/SuggestedQuestionsTab.tsx +24 -0
- package/src/features/assistant-message/components/parts/tabs/UsersTab.tsx +142 -0
- package/src/features/assistant-message/data/AssistantMessage.ts +20 -0
- package/src/features/assistant-message/data/AssistantMessageInterface.ts +2 -0
- package/src/features/assistant-message/data/AssistantMessageService.ts +13 -4
- package/src/features/assistant-message/data/__tests__/AssistantMessage.citations.spec.ts +65 -0
- package/src/features/assistant-message/data/__tests__/AssistantMessage.spec.ts +8 -0
- package/src/features/chunk/ChunkModule.ts +18 -0
- package/src/features/chunk/data/Chunk.ts +49 -0
- package/src/features/chunk/data/ChunkInput.ts +3 -0
- package/src/features/chunk/data/ChunkInterface.ts +18 -0
- package/src/features/chunk/data/__tests__/Chunk.spec.ts +83 -0
- package/src/features/chunk/data/index.ts +3 -0
- package/src/features/chunk/index.ts +2 -0
- package/src/features/rbac/components/RbacByRoleContainer.tsx +270 -0
- package/src/features/rbac/index.ts +1 -0
- package/dist/chunk-5IEWLLLD.js.map +0 -1
- package/dist/chunk-BKM5U3DE.mjs.map +0 -1
- package/dist/chunk-ENRSFVOS.mjs.map +0 -1
- package/dist/chunk-ZDP3MBUI.js.map +0 -1
- package/src/features/assistant-message/components/parts/ReferenceBadges.tsx +0 -46
- package/src/features/assistant-message/components/parts/SuggestedFollowUps.tsx +0 -52
- package/src/features/assistant-message/components/parts/__tests__/ReferenceBadges.spec.tsx +0 -59
- package/src/features/assistant-message/components/parts/__tests__/SuggestedFollowUps.spec.tsx +0 -29
- /package/dist/{BlockNoteEditor-6CBDTVKV.mjs.map → BlockNoteEditor-DCQA2PNW.mjs.map} +0 -0
- /package/dist/{chunk-MEWXQEVE.mjs.map → chunk-PV5V6CVW.mjs.map} +0 -0
package/dist/index.mjs
CHANGED
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
getWaitlistConfig,
|
|
18
18
|
isReferralEnabled,
|
|
19
19
|
isRolesConfigured
|
|
20
|
-
} from "./chunk-
|
|
20
|
+
} from "./chunk-PV5V6CVW.mjs";
|
|
21
21
|
import {
|
|
22
22
|
AVAILABLE_OAUTH_SCOPES,
|
|
23
23
|
AbstractApiData,
|
|
@@ -44,6 +44,8 @@ import {
|
|
|
44
44
|
BillingService,
|
|
45
45
|
BlockNoteDiffUtil,
|
|
46
46
|
BlockNoteWordDiffRendererUtil,
|
|
47
|
+
Chunk,
|
|
48
|
+
ChunkModule,
|
|
47
49
|
ClientAbstractService,
|
|
48
50
|
ClientHttpMethod,
|
|
49
51
|
Company,
|
|
@@ -195,7 +197,7 @@ import {
|
|
|
195
197
|
useComposedRefs,
|
|
196
198
|
useIsMobile,
|
|
197
199
|
userObjectSchema
|
|
198
|
-
} from "./chunk-
|
|
200
|
+
} from "./chunk-6UMB5LTQ.mjs";
|
|
199
201
|
import "./chunk-AUXK7QSA.mjs";
|
|
200
202
|
import "./chunk-C7C7VY4F.mjs";
|
|
201
203
|
import {
|
|
@@ -238,6 +240,8 @@ export {
|
|
|
238
240
|
BlockNoteDiffUtil,
|
|
239
241
|
BlockNoteWordDiffRendererUtil,
|
|
240
242
|
COMPANY_ADMINISTRATOR_ROLE_ID,
|
|
243
|
+
Chunk,
|
|
244
|
+
ChunkModule,
|
|
241
245
|
ClientAbstractService,
|
|
242
246
|
ClientHttpMethod,
|
|
243
247
|
Company,
|
package/dist/server/index.js
CHANGED
|
@@ -15,7 +15,7 @@ var _chunk3ZPK4QOBjs = require('../chunk-3ZPK4QOB.js');
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
var
|
|
18
|
+
var _chunkN4YZ45SKjs = require('../chunk-N4YZ45SK.js');
|
|
19
19
|
require('../chunk-LXKSUWAV.js');
|
|
20
20
|
require('../chunk-IBS6NI7D.js');
|
|
21
21
|
|
|
@@ -86,7 +86,7 @@ var ServerSession = class {
|
|
|
86
86
|
if (!rawModules) return false;
|
|
87
87
|
const modules = JSON.parse(_pako2.default.ungzip(Buffer.from(rawModules, "base64"), { to: "string" }));
|
|
88
88
|
const selectedModule = modules.find((module) => module.id === params.module.moduleId);
|
|
89
|
-
return
|
|
89
|
+
return _chunkN4YZ45SKjs.checkPermissionsFromServer.call(void 0, {
|
|
90
90
|
module: params.module,
|
|
91
91
|
action: params.action,
|
|
92
92
|
data: params.data,
|
|
@@ -296,5 +296,5 @@ _chunk7QVYU63Ejs.__name.call(void 0, ServerJsonApiDelete, "ServerJsonApiDelete")
|
|
|
296
296
|
|
|
297
297
|
|
|
298
298
|
|
|
299
|
-
exports.ServerAuthService =
|
|
299
|
+
exports.ServerAuthService = _chunkN4YZ45SKjs.AuthService; exports.ServerCompanyService = _chunkN4YZ45SKjs.CompanyService; exports.ServerContentService = _chunkN4YZ45SKjs.ContentService; exports.ServerFeatureService = _chunkN4YZ45SKjs.FeatureService; exports.ServerJsonApiDelete = ServerJsonApiDelete; exports.ServerJsonApiGet = ServerJsonApiGet; exports.ServerJsonApiPatch = ServerJsonApiPatch; exports.ServerJsonApiPost = ServerJsonApiPost; exports.ServerJsonApiPut = ServerJsonApiPut; exports.ServerNotificationService = _chunkN4YZ45SKjs.NotificationService; exports.ServerPushService = _chunkN4YZ45SKjs.PushService; exports.ServerRoleService = _chunkN4YZ45SKjs.RoleService; exports.ServerS3Service = _chunkN4YZ45SKjs.S3Service; exports.ServerSession = ServerSession; exports.ServerUserService = _chunkN4YZ45SKjs.UserService; exports.configureServerJsonApi = configureServerJsonApi; exports.getServerApiUrl = getServerApiUrl; exports.getServerAppUrl = getServerAppUrl; exports.getServerToken = _chunkYUO55Q5Ajs.getServerToken; exports.getServerTrackablePages = getServerTrackablePages; exports.invalidateCacheTag = invalidateCacheTag; exports.invalidateCacheTags = invalidateCacheTags; exports.serverRequest = _chunk3ZPK4QOBjs.serverRequest;
|
|
300
300
|
//# sourceMappingURL=index.js.map
|
package/dist/server/index.mjs
CHANGED
package/package.json
CHANGED
package/src/components/index.ts
CHANGED
|
@@ -31,6 +31,7 @@ export * from "../features/user/components";
|
|
|
31
31
|
export * from "../features/oauth/components";
|
|
32
32
|
export * from "../features/waitlist/components";
|
|
33
33
|
export { RbacContainer } from "../features/rbac/components/RbacContainer";
|
|
34
|
+
export { RbacByRoleContainer } from "../features/rbac/components/RbacByRoleContainer";
|
|
34
35
|
export { RbacPermissionCell } from "../features/rbac/components/RbacPermissionCell";
|
|
35
36
|
export { RbacPermissionPicker } from "../features/rbac/components/RbacPermissionPicker";
|
|
36
37
|
|
package/src/core/index.ts
CHANGED
|
@@ -65,6 +65,8 @@ export * from "../features/assistant/AssistantModule";
|
|
|
65
65
|
export * from "../features/assistant/data";
|
|
66
66
|
export * from "../features/assistant-message/AssistantMessageModule";
|
|
67
67
|
export * from "../features/assistant-message/data";
|
|
68
|
+
export * from "../features/chunk/ChunkModule";
|
|
69
|
+
export * from "../features/chunk/data";
|
|
68
70
|
export * from "../features/feature/data";
|
|
69
71
|
export * from "../features/feature/feature.module";
|
|
70
72
|
export * from "../features/module";
|
|
@@ -18,6 +18,7 @@ export interface FoundationModuleDefinitions {
|
|
|
18
18
|
HowTo: ModuleWithPermissions;
|
|
19
19
|
Assistant: ModuleWithPermissions;
|
|
20
20
|
AssistantMessage: ModuleWithPermissions;
|
|
21
|
+
Chunk: ModuleWithPermissions;
|
|
21
22
|
// Billing modules - READ: all users, UPDATE: CompanyAdministrator, ADMIN: Administrator
|
|
22
23
|
Billing: ModuleWithPermissions;
|
|
23
24
|
StripeCustomer: ModuleWithPermissions;
|
|
@@ -22,7 +22,7 @@ export function AssistantThread({ messages, sending, status, onSelectFollowUp, f
|
|
|
22
22
|
}, [messages.length, sending]);
|
|
23
23
|
|
|
24
24
|
return (
|
|
25
|
-
<div className="flex-1 overflow-y-auto px-6 py-5">
|
|
25
|
+
<div className="flex-1 min-w-0 overflow-x-hidden overflow-y-auto px-6 py-5">
|
|
26
26
|
<MessageList
|
|
27
27
|
messages={messages}
|
|
28
28
|
onSelectFollowUp={onSelectFollowUp}
|
|
@@ -12,6 +12,7 @@ export const AssistantMessageModule = (factory: ModuleFactory) =>
|
|
|
12
12
|
icon: MessageSquareIcon,
|
|
13
13
|
inclusions: {
|
|
14
14
|
lists: {
|
|
15
|
+
types: [`references`, `citations`, `citations.source`, `citations.source.author`],
|
|
15
16
|
fields: [
|
|
16
17
|
createJsonApiInclusion("assistant-messages", [
|
|
17
18
|
`role`,
|
|
@@ -21,7 +22,10 @@ export const AssistantMessageModule = (factory: ModuleFactory) =>
|
|
|
21
22
|
`inputTokens`,
|
|
22
23
|
`outputTokens`,
|
|
23
24
|
`references`,
|
|
25
|
+
`citations`,
|
|
24
26
|
]),
|
|
27
|
+
createJsonApiInclusion("chunks", [`content`, `nodeId`, `nodeType`, `imagePath`, `source`]),
|
|
28
|
+
createJsonApiInclusion("users", [`name`, `avatar`]),
|
|
25
29
|
],
|
|
26
30
|
},
|
|
27
31
|
},
|
|
@@ -5,8 +5,7 @@ import { Sparkles, AlertCircle } from "lucide-react";
|
|
|
5
5
|
import ReactMarkdown from "react-markdown";
|
|
6
6
|
import remarkGfm from "remark-gfm";
|
|
7
7
|
import type { AssistantMessageInterface } from "../data/AssistantMessageInterface";
|
|
8
|
-
import {
|
|
9
|
-
import { SuggestedFollowUps } from "./parts/SuggestedFollowUps";
|
|
8
|
+
import { MessageSourcesContainer } from "./parts/MessageSourcesContainer";
|
|
10
9
|
|
|
11
10
|
interface Props {
|
|
12
11
|
message: AssistantMessageInterface;
|
|
@@ -41,7 +40,7 @@ export function MessageItem({ message, isLatestAssistant, onSelectFollowUp, fail
|
|
|
41
40
|
}
|
|
42
41
|
|
|
43
42
|
return (
|
|
44
|
-
<div className="flex max-w-[78%] flex-col gap-1.5">
|
|
43
|
+
<div className="flex min-w-0 max-w-[78%] flex-col gap-1.5">
|
|
45
44
|
<div className="text-muted-foreground flex items-center gap-2 pl-1 text-xs">
|
|
46
45
|
<span className="flex h-4 w-4 items-center justify-center rounded-full bg-gradient-to-br from-blue-400 to-violet-500 text-white">
|
|
47
46
|
<Sparkles className="h-2.5 w-2.5" />
|
|
@@ -51,10 +50,11 @@ export function MessageItem({ message, isLatestAssistant, onSelectFollowUp, fail
|
|
|
51
50
|
<div className="bg-muted text-foreground rounded-2xl rounded-bl-sm px-3.5 py-2.5 text-sm leading-relaxed">
|
|
52
51
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>{message.content}</ReactMarkdown>
|
|
53
52
|
</div>
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
53
|
+
<MessageSourcesContainer
|
|
54
|
+
message={message}
|
|
55
|
+
isLatestAssistant={isLatestAssistant}
|
|
56
|
+
onSelectFollowUp={onSelectFollowUp}
|
|
57
|
+
/>
|
|
58
58
|
</div>
|
|
59
59
|
);
|
|
60
60
|
}
|
|
@@ -8,7 +8,7 @@ import type { ApiDataInterface } from "../../../../core";
|
|
|
8
8
|
import type { AssistantMessageInterface } from "../../data/AssistantMessageInterface";
|
|
9
9
|
import { MessageItem } from "../MessageItem";
|
|
10
10
|
|
|
11
|
-
// Test-only account model to exercise
|
|
11
|
+
// Test-only account model to exercise the sources panel references list
|
|
12
12
|
class TestAccount extends AbstractApiData {
|
|
13
13
|
static identifierFields: string[] = ["name"];
|
|
14
14
|
rehydrate(data: any): this {
|
|
@@ -51,6 +51,7 @@ function buildMessageStub(p: {
|
|
|
51
51
|
content: p.content ?? "",
|
|
52
52
|
position: 0,
|
|
53
53
|
references: p.references ?? [],
|
|
54
|
+
citations: [],
|
|
54
55
|
suggestedQuestions: p.suggestedQuestions ?? [],
|
|
55
56
|
createdAt: new Date(),
|
|
56
57
|
updatedAt: new Date(),
|
|
@@ -66,7 +67,7 @@ describe("MessageItem", () => {
|
|
|
66
67
|
expect(screen.queryByText("features.assistant.agent_name")).not.toBeInTheDocument();
|
|
67
68
|
});
|
|
68
69
|
|
|
69
|
-
it("assistant message renders
|
|
70
|
+
it("assistant message renders agent label and the sources panel toggle when sources exist", () => {
|
|
70
71
|
const msg = buildMessageStub({
|
|
71
72
|
role: "assistant",
|
|
72
73
|
content: "**bold** reply",
|
|
@@ -75,14 +76,17 @@ describe("MessageItem", () => {
|
|
|
75
76
|
});
|
|
76
77
|
render(<MessageItem message={msg} isLatestAssistant={true} onSelectFollowUp={vi.fn()} />);
|
|
77
78
|
expect(screen.getByText("features.assistant.agent_name")).toBeInTheDocument();
|
|
78
|
-
|
|
79
|
-
expect(screen.getByRole("button", { name: /
|
|
79
|
+
// The MessageSourcesPanel toggle uses the message.sources.toggle translation key.
|
|
80
|
+
expect(screen.getByRole("button", { name: /sources\.toggle/i })).toBeInTheDocument();
|
|
80
81
|
});
|
|
81
82
|
|
|
82
|
-
it("assistant message NOT latest: no
|
|
83
|
+
it("assistant message NOT latest with only suggestions: no sources panel rendered", () => {
|
|
83
84
|
const msg = buildMessageStub({ role: "assistant", content: "prior", suggestedQuestions: ["x"] });
|
|
84
|
-
render(<MessageItem message={msg} isLatestAssistant={false} onSelectFollowUp={vi.fn()} />);
|
|
85
|
-
|
|
85
|
+
const { container } = render(<MessageItem message={msg} isLatestAssistant={false} onSelectFollowUp={vi.fn()} />);
|
|
86
|
+
// No sources/citations/references and suggestions are gated by isLatestAssistant -> panel renders nothing.
|
|
87
|
+
expect(screen.queryByRole("button", { name: /sources\.toggle/i })).not.toBeInTheDocument();
|
|
88
|
+
// Sanity: the assistant bubble itself still rendered.
|
|
89
|
+
expect(container).not.toBeEmptyDOMElement();
|
|
86
90
|
});
|
|
87
91
|
|
|
88
92
|
it("failed user message: renders retry control and calls onRetry with the id", async () => {
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useMemo, useState } from "react";
|
|
4
|
+
import { ClientAbstractService, ClientHttpMethod, EndpointCreator, ModuleRegistry } from "../../../../core";
|
|
5
|
+
import type { ApiDataInterface, ApiRequestDataTypeInterface } from "../../../../core";
|
|
6
|
+
import type { AssistantMessageInterface } from "../../data/AssistantMessageInterface";
|
|
7
|
+
import { MessageSourcesPanel } from "./MessageSourcesPanel";
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
message: AssistantMessageInterface;
|
|
11
|
+
isLatestAssistant: boolean;
|
|
12
|
+
onSelectFollowUp: (q: string) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class SourcesFetcher extends ClientAbstractService {
|
|
16
|
+
static async findManyByIds(params: {
|
|
17
|
+
module: ApiRequestDataTypeInterface;
|
|
18
|
+
idsParam: string;
|
|
19
|
+
ids: string[];
|
|
20
|
+
}): Promise<ApiDataInterface[]> {
|
|
21
|
+
const endpoint = new EndpointCreator({ endpoint: params.module });
|
|
22
|
+
endpoint.addAdditionalParam(params.idsParam, params.ids.join(","));
|
|
23
|
+
if (params.module.inclusions?.lists?.fields) endpoint.limitToFields(params.module.inclusions.lists.fields);
|
|
24
|
+
if (params.module.inclusions?.lists?.types) endpoint.limitToType(params.module.inclusions.lists.types);
|
|
25
|
+
return this.callApi<ApiDataInterface[]>({
|
|
26
|
+
type: params.module,
|
|
27
|
+
method: ClientHttpMethod.GET,
|
|
28
|
+
endpoint: endpoint.generate(),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Library-level data wrapper around `MessageSourcesPanel`. Reads
|
|
35
|
+
* `chunk.nodeId` / `chunk.nodeType` from the message's citations, dynamically
|
|
36
|
+
* resolves the matching module via `ModuleRegistry.findByModelName(nodeType)`,
|
|
37
|
+
* fetches the entities in one request per type, and supplies them (plus their
|
|
38
|
+
* deduplicated `author` users) to the presentational panel.
|
|
39
|
+
*/
|
|
40
|
+
export function MessageSourcesContainer({ message, isLatestAssistant, onSelectFollowUp }: Props) {
|
|
41
|
+
// Group nodeIds per nodeType so we issue one request per source type.
|
|
42
|
+
const groups = useMemo(() => {
|
|
43
|
+
const map = new Map<string, Set<string>>();
|
|
44
|
+
for (const chunk of message.citations) {
|
|
45
|
+
if (!chunk.nodeType || !chunk.nodeId) continue;
|
|
46
|
+
let bucket = map.get(chunk.nodeType);
|
|
47
|
+
if (!bucket) {
|
|
48
|
+
bucket = new Set<string>();
|
|
49
|
+
map.set(chunk.nodeType, bucket);
|
|
50
|
+
}
|
|
51
|
+
bucket.add(chunk.nodeId);
|
|
52
|
+
}
|
|
53
|
+
return map;
|
|
54
|
+
}, [message.citations]);
|
|
55
|
+
|
|
56
|
+
// Flatten to a deterministic key so useEffect re-runs when ids change.
|
|
57
|
+
const groupsKey = useMemo(() => {
|
|
58
|
+
const parts: string[] = [];
|
|
59
|
+
for (const [nodeType, ids] of groups) {
|
|
60
|
+
parts.push(`${nodeType}:${Array.from(ids).sort().join(",")}`);
|
|
61
|
+
}
|
|
62
|
+
return parts.sort().join("|");
|
|
63
|
+
}, [groups]);
|
|
64
|
+
|
|
65
|
+
const [resolved, setResolved] = useState<ApiDataInterface[]>([]);
|
|
66
|
+
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
if (groups.size === 0) {
|
|
69
|
+
setResolved([]);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
let cancelled = false;
|
|
73
|
+
const requests: Promise<ApiDataInterface[]>[] = [];
|
|
74
|
+
for (const [nodeType, idSet] of groups) {
|
|
75
|
+
let module: ApiRequestDataTypeInterface | undefined;
|
|
76
|
+
try {
|
|
77
|
+
module = ModuleRegistry.findByModelName(nodeType);
|
|
78
|
+
} catch {
|
|
79
|
+
continue; // No registered module for this nodeType — skip silently.
|
|
80
|
+
}
|
|
81
|
+
const idsParam = `${(module as any).nodeName ?? module.name.replace(/s$/, "")}Ids`;
|
|
82
|
+
requests.push(
|
|
83
|
+
SourcesFetcher.findManyByIds({
|
|
84
|
+
module,
|
|
85
|
+
idsParam,
|
|
86
|
+
ids: Array.from(idSet),
|
|
87
|
+
}).catch((error) => {
|
|
88
|
+
console.error(`Failed to load sources for ${nodeType}:`, error);
|
|
89
|
+
return [] as ApiDataInterface[];
|
|
90
|
+
}),
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
Promise.all(requests).then((results) => {
|
|
94
|
+
if (cancelled) return;
|
|
95
|
+
setResolved(results.flat());
|
|
96
|
+
});
|
|
97
|
+
return () => {
|
|
98
|
+
cancelled = true;
|
|
99
|
+
};
|
|
100
|
+
}, [groupsKey, groups]);
|
|
101
|
+
|
|
102
|
+
const sources = useMemo(() => {
|
|
103
|
+
const map = new Map<string, ApiDataInterface>();
|
|
104
|
+
for (const entity of resolved) map.set(entity.id, entity);
|
|
105
|
+
return map;
|
|
106
|
+
}, [resolved]);
|
|
107
|
+
|
|
108
|
+
const users = useMemo(() => {
|
|
109
|
+
const userMap = new Map<string, ApiDataInterface>();
|
|
110
|
+
for (const entity of resolved) {
|
|
111
|
+
// Content.author getter throws when missing; tolerate either shape.
|
|
112
|
+
const author =
|
|
113
|
+
(entity as any)._author ??
|
|
114
|
+
(() => {
|
|
115
|
+
try {
|
|
116
|
+
return (entity as any).author;
|
|
117
|
+
} catch {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
})();
|
|
121
|
+
if (author?.id) userMap.set(author.id, author);
|
|
122
|
+
}
|
|
123
|
+
return Array.from(userMap.values());
|
|
124
|
+
}, [resolved]);
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<MessageSourcesPanel
|
|
128
|
+
message={message}
|
|
129
|
+
isLatestAssistant={isLatestAssistant}
|
|
130
|
+
onSelectFollowUp={onSelectFollowUp}
|
|
131
|
+
sources={sources}
|
|
132
|
+
users={users}
|
|
133
|
+
/>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useMemo, useState } from "react";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
import { ChevronDown, ChevronRight } from "lucide-react";
|
|
6
|
+
import type { ApiDataInterface } from "../../../../core";
|
|
7
|
+
import { ModuleRegistry } from "../../../../core/registry/ModuleRegistry";
|
|
8
|
+
import type { AssistantMessageInterface } from "../../data/AssistantMessageInterface";
|
|
9
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../../../shadcnui/ui/tabs";
|
|
10
|
+
import { ReferencesTab } from "./tabs/ReferencesTab";
|
|
11
|
+
import { CitationsTab } from "./tabs/CitationsTab";
|
|
12
|
+
import { ContentsTab } from "./tabs/ContentsTab";
|
|
13
|
+
import { UsersTab } from "./tabs/UsersTab";
|
|
14
|
+
import { SuggestedQuestionsTab } from "./tabs/SuggestedQuestionsTab";
|
|
15
|
+
|
|
16
|
+
interface Props {
|
|
17
|
+
message: AssistantMessageInterface;
|
|
18
|
+
isLatestAssistant: boolean;
|
|
19
|
+
onSelectFollowUp: (q: string) => void;
|
|
20
|
+
/**
|
|
21
|
+
* Map of resolved source entities keyed by `chunk.nodeId`. When provided, the
|
|
22
|
+
* Contents and Citations tabs use these to render real entity names. The
|
|
23
|
+
* library is presentational; the app fetches and supplies these.
|
|
24
|
+
*/
|
|
25
|
+
sources?: Map<string, ApiDataInterface>;
|
|
26
|
+
/**
|
|
27
|
+
* Deduplicated list of authors derived from the resolved sources. When
|
|
28
|
+
* provided, the Users tab is populated from this list.
|
|
29
|
+
*/
|
|
30
|
+
users?: ApiDataInterface[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type TabKey = "suggested" | "references" | "citations" | "contents" | "users";
|
|
34
|
+
|
|
35
|
+
export function MessageSourcesPanel({ message, isLatestAssistant, onSelectFollowUp, sources, users }: Props) {
|
|
36
|
+
const t = useTranslations();
|
|
37
|
+
|
|
38
|
+
// Filter out references whose module has no pageUrl: those are
|
|
39
|
+
// intermediary/relational nodes (e.g., BomItem, BomTreeNode) that the user
|
|
40
|
+
// shouldn't navigate to directly.
|
|
41
|
+
const visibleReferences = useMemo(
|
|
42
|
+
() =>
|
|
43
|
+
message.references.filter((ref) => {
|
|
44
|
+
try {
|
|
45
|
+
return !!ModuleRegistry.findByName(ref.type).pageUrl;
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}),
|
|
50
|
+
[message.references],
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
const refsCount = visibleReferences.length;
|
|
54
|
+
const citationsCount = message.citations.length;
|
|
55
|
+
const suggestionsCount = isLatestAssistant ? message.suggestedQuestions.length : 0;
|
|
56
|
+
|
|
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).
|
|
61
|
+
const ids = new Set<string>();
|
|
62
|
+
for (const c of message.citations) {
|
|
63
|
+
if (c.nodeId) ids.add(c.nodeId);
|
|
64
|
+
}
|
|
65
|
+
return ids.size;
|
|
66
|
+
}, [message.citations, sources]);
|
|
67
|
+
|
|
68
|
+
const usersCount = users?.length ?? 0;
|
|
69
|
+
|
|
70
|
+
const total = refsCount + citationsCount + contentsCount + usersCount + suggestionsCount;
|
|
71
|
+
|
|
72
|
+
const visibleTabs: TabKey[] = [];
|
|
73
|
+
if (suggestionsCount > 0) visibleTabs.push("suggested");
|
|
74
|
+
if (refsCount > 0) visibleTabs.push("references");
|
|
75
|
+
if (citationsCount > 0) visibleTabs.push("citations");
|
|
76
|
+
if (contentsCount > 0) visibleTabs.push("contents");
|
|
77
|
+
if (usersCount > 0) visibleTabs.push("users");
|
|
78
|
+
|
|
79
|
+
const autoOpen = isLatestAssistant && suggestionsCount > 0;
|
|
80
|
+
const initialTab: TabKey | undefined = autoOpen ? "suggested" : visibleTabs[0];
|
|
81
|
+
const [open, setOpen] = useState(autoOpen);
|
|
82
|
+
const [active, setActive] = useState<TabKey | undefined>(initialTab);
|
|
83
|
+
|
|
84
|
+
if (total === 0) return null;
|
|
85
|
+
|
|
86
|
+
const panelId = `sources-panel-${message.id}`;
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<div className="mt-2 w-full min-w-0">
|
|
90
|
+
<button
|
|
91
|
+
type="button"
|
|
92
|
+
onClick={() => setOpen((v) => !v)}
|
|
93
|
+
aria-expanded={open}
|
|
94
|
+
aria-controls={panelId}
|
|
95
|
+
className="text-primary inline-flex items-center gap-1 text-xs font-medium"
|
|
96
|
+
>
|
|
97
|
+
{open ? <ChevronDown className="h-3 w-3" /> : <ChevronRight className="h-3 w-3" />}
|
|
98
|
+
{open
|
|
99
|
+
? t("features.assistant.message.sources.toggle_hide")
|
|
100
|
+
: t("features.assistant.message.sources.toggle", { count: total })}
|
|
101
|
+
</button>
|
|
102
|
+
|
|
103
|
+
{open && (
|
|
104
|
+
<div id={panelId} className="mt-2 w-full min-w-0">
|
|
105
|
+
<Tabs value={active} onValueChange={(v) => setActive(v as TabKey)}>
|
|
106
|
+
<TabsList>
|
|
107
|
+
{visibleTabs.map((key) => (
|
|
108
|
+
<TabsTrigger key={key} value={key}>
|
|
109
|
+
{t(`features.assistant.message.sources.tabs.${key === "suggested" ? "suggested_questions" : key}`)}
|
|
110
|
+
<span className="text-muted-foreground ml-1.5 text-[10px]">
|
|
111
|
+
{key === "suggested" && suggestionsCount}
|
|
112
|
+
{key === "references" && refsCount}
|
|
113
|
+
{key === "citations" && citationsCount}
|
|
114
|
+
{key === "contents" && contentsCount}
|
|
115
|
+
{key === "users" && usersCount}
|
|
116
|
+
</span>
|
|
117
|
+
</TabsTrigger>
|
|
118
|
+
))}
|
|
119
|
+
</TabsList>
|
|
120
|
+
|
|
121
|
+
{suggestionsCount > 0 && (
|
|
122
|
+
<TabsContent value="suggested">
|
|
123
|
+
<SuggestedQuestionsTab questions={message.suggestedQuestions} onSelect={onSelectFollowUp} />
|
|
124
|
+
</TabsContent>
|
|
125
|
+
)}
|
|
126
|
+
{refsCount > 0 && (
|
|
127
|
+
<TabsContent value="references">
|
|
128
|
+
<ReferencesTab references={visibleReferences} />
|
|
129
|
+
</TabsContent>
|
|
130
|
+
)}
|
|
131
|
+
{citationsCount > 0 && (
|
|
132
|
+
<TabsContent value="citations">
|
|
133
|
+
<CitationsTab citations={message.citations} sources={sources} />
|
|
134
|
+
</TabsContent>
|
|
135
|
+
)}
|
|
136
|
+
{contentsCount > 0 && (
|
|
137
|
+
<TabsContent value="contents">
|
|
138
|
+
<ContentsTab citations={message.citations} sources={sources} />
|
|
139
|
+
</TabsContent>
|
|
140
|
+
)}
|
|
141
|
+
{usersCount > 0 && (
|
|
142
|
+
<TabsContent value="users">
|
|
143
|
+
<UsersTab users={users ?? []} citations={message.citations} sources={sources} />
|
|
144
|
+
</TabsContent>
|
|
145
|
+
)}
|
|
146
|
+
</Tabs>
|
|
147
|
+
</div>
|
|
148
|
+
)}
|
|
149
|
+
</div>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
interface Props {
|
|
4
|
+
/** 0-1 (decimal) or 0-100 (percent). Auto-detected. */
|
|
5
|
+
value: number;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function RelevanceMeter({ value, className = "" }: Props) {
|
|
10
|
+
const pct = Math.max(0, Math.min(100, value <= 1 ? value * 100 : value));
|
|
11
|
+
const label = `${Math.round(pct)}%`;
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
role="meter"
|
|
15
|
+
aria-valuenow={Math.round(pct)}
|
|
16
|
+
aria-valuemin={0}
|
|
17
|
+
aria-valuemax={100}
|
|
18
|
+
aria-label={`Relevance ${label}`}
|
|
19
|
+
className={`bg-muted relative mx-auto flex h-5 w-20 items-center justify-center overflow-hidden rounded border ${className}`}
|
|
20
|
+
>
|
|
21
|
+
<div className="bg-accent absolute top-0 left-0 h-full" style={{ width: `${pct}%` }} />
|
|
22
|
+
<span
|
|
23
|
+
className={`relative text-xs ${pct < 40 ? "text-muted-foreground" : "text-accent-foreground font-semibold"}`}
|
|
24
|
+
>
|
|
25
|
+
{label}
|
|
26
|
+
</span>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
package/src/features/assistant-message/components/parts/__tests__/MessageSourcesPanel.spec.tsx
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { ModuleRegistry } from "../../../../../core/registry/ModuleRegistry";
|
|
4
|
+
import { MessageSourcesPanel } from "../MessageSourcesPanel";
|
|
5
|
+
|
|
6
|
+
const baseMessage: any = {
|
|
7
|
+
id: "m1",
|
|
8
|
+
role: "assistant",
|
|
9
|
+
content: "x",
|
|
10
|
+
references: [],
|
|
11
|
+
citations: [],
|
|
12
|
+
suggestedQuestions: [],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
beforeAll(() => {
|
|
16
|
+
// Register a stub Order module so reference filtering by `pageUrl` succeeds in tests.
|
|
17
|
+
ModuleRegistry.register("Order" as any, { name: "orders", pageUrl: "/orders" } as any);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
afterAll(() => {
|
|
21
|
+
// No public unregister; safe to leave the stub since other suites do not collide on this name.
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("MessageSourcesPanel", () => {
|
|
25
|
+
it("renders nothing when message has no sources", () => {
|
|
26
|
+
const { container } = render(
|
|
27
|
+
<MessageSourcesPanel message={baseMessage} isLatestAssistant={false} onSelectFollowUp={vi.fn()} />,
|
|
28
|
+
);
|
|
29
|
+
expect(container).toBeEmptyDOMElement();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("renders the toggle when at least one source exists", () => {
|
|
33
|
+
const msg = {
|
|
34
|
+
...baseMessage,
|
|
35
|
+
references: [{ id: "r1", type: "orders", identifier: "ORD-001" }],
|
|
36
|
+
};
|
|
37
|
+
render(<MessageSourcesPanel message={msg} isLatestAssistant={false} onSelectFollowUp={vi.fn()} />);
|
|
38
|
+
// Mocked next-intl returns the translation key verbatim,
|
|
39
|
+
// so the toggle button name contains "sources.toggle".
|
|
40
|
+
expect(screen.getByRole("button", { name: /sources\.toggle/i })).toBeInTheDocument();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("hides references whose module has no pageUrl (middle entities)", () => {
|
|
44
|
+
ModuleRegistry.register("BomItem" as any, { name: "bom-items" } as any);
|
|
45
|
+
const msg = {
|
|
46
|
+
...baseMessage,
|
|
47
|
+
references: [{ id: "b1", type: "bom-items", identifier: "BI-1" }],
|
|
48
|
+
};
|
|
49
|
+
const { container } = render(
|
|
50
|
+
<MessageSourcesPanel message={msg} isLatestAssistant={false} onSelectFollowUp={vi.fn()} />,
|
|
51
|
+
);
|
|
52
|
+
expect(container).toBeEmptyDOMElement();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("auto-opens with Suggested questions tab when isLatestAssistant and questions exist", () => {
|
|
56
|
+
const msg = { ...baseMessage, suggestedQuestions: ["Why?"] };
|
|
57
|
+
render(<MessageSourcesPanel message={msg} isLatestAssistant={true} onSelectFollowUp={vi.fn()} />);
|
|
58
|
+
const tab = screen.getByRole("tab", { name: /suggested/i });
|
|
59
|
+
expect(tab).toBeInTheDocument();
|
|
60
|
+
// Base UI uses data-active on the active tab/panel; aria-selected reflects active state too.
|
|
61
|
+
expect(tab.getAttribute("aria-selected")).toBe("true");
|
|
62
|
+
expect(screen.getByText("Why?")).toBeInTheDocument();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("does not show Suggested questions tab on non-latest message", () => {
|
|
66
|
+
const msg = { ...baseMessage, suggestedQuestions: ["Why?"] };
|
|
67
|
+
render(<MessageSourcesPanel message={msg} isLatestAssistant={false} onSelectFollowUp={vi.fn()} />);
|
|
68
|
+
expect(screen.queryByRole("tab", { name: /suggested/i })).not.toBeInTheDocument();
|
|
69
|
+
});
|
|
70
|
+
});
|