@carlonicora/nextjs-jsonapi 1.77.3 → 1.78.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AssistantInterface-BYgI5z1-.d.mts +12 -0
- package/dist/AssistantInterface-DfDcz0gJ.d.ts +12 -0
- package/dist/AssistantMessageInterface-D0Kwf8CR.d.mts +36 -0
- package/dist/AssistantMessageInterface-DS_tyJTV.d.ts +36 -0
- package/dist/{BlockNoteEditor-UB7T7V67.js → BlockNoteEditor-2G5UYALC.js} +14 -14
- package/dist/{BlockNoteEditor-UB7T7V67.js.map → BlockNoteEditor-2G5UYALC.js.map} +1 -1
- package/dist/{BlockNoteEditor-7HAAXN3H.mjs → BlockNoteEditor-JXK3JGKJ.mjs} +4 -4
- package/dist/billing/index.js +346 -346
- package/dist/billing/index.mjs +3 -3
- package/dist/{chunk-FKLP4NED.js → chunk-FDJQRIMY.js} +320 -18
- package/dist/chunk-FDJQRIMY.js.map +1 -0
- package/dist/{chunk-XI35ALWY.mjs → chunk-I65SSQ5Z.mjs} +303 -1
- package/dist/chunk-I65SSQ5Z.mjs.map +1 -0
- package/dist/{chunk-F44ET4AC.mjs → chunk-NB6TIKHK.mjs} +2087 -1463
- package/dist/chunk-NB6TIKHK.mjs.map +1 -0
- package/dist/{chunk-JOJZRGZL.mjs → chunk-NZOUEN67.mjs} +2 -2
- package/dist/{chunk-OTZEXASK.js → chunk-X4YDETTD.js} +11 -11
- package/dist/{chunk-OTZEXASK.js.map → chunk-X4YDETTD.js.map} +1 -1
- package/dist/{chunk-CV7UOUKQ.js → chunk-ZEDB6JVB.js} +1356 -732
- package/dist/chunk-ZEDB6JVB.js.map +1 -0
- package/dist/client/index.js +4 -4
- package/dist/client/index.mjs +3 -3
- package/dist/components/index.d.mts +21 -2
- package/dist/components/index.d.ts +21 -2
- package/dist/components/index.js +10 -4
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +9 -3
- package/dist/contexts/index.d.mts +26 -2
- package/dist/contexts/index.d.ts +26 -2
- package/dist/contexts/index.js +8 -4
- package/dist/contexts/index.js.map +1 -1
- package/dist/contexts/index.mjs +7 -3
- package/dist/core/index.d.mts +110 -3
- package/dist/core/index.d.ts +110 -3
- package/dist/core/index.js +14 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +13 -1
- package/dist/index.d.mts +3 -2
- package/dist/index.d.ts +3 -2
- package/dist/index.js +15 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +14 -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 +2 -0
- package/src/contexts/index.ts +1 -0
- package/src/core/index.ts +4 -0
- package/src/core/registry/ModuleRegistry.ts +9 -0
- package/src/features/assistant/AssistantModule.ts +19 -0
- package/src/features/assistant/components/containers/AssistantContainer.tsx +56 -0
- package/src/features/assistant/components/containers/__tests__/AssistantContainer.spec.tsx +101 -0
- package/src/features/assistant/components/index.ts +1 -0
- package/src/features/assistant/components/parts/AssistantComposer.tsx +56 -0
- package/src/features/assistant/components/parts/AssistantEmptyState.tsx +47 -0
- package/src/features/assistant/components/parts/AssistantSidebar.tsx +64 -0
- package/src/features/assistant/components/parts/AssistantStatusLine.tsx +19 -0
- package/src/features/assistant/components/parts/AssistantThread.tsx +36 -0
- package/src/features/assistant/components/parts/AssistantThreadHeader.tsx +91 -0
- package/src/features/assistant/components/parts/__tests__/AssistantComposer.spec.tsx +32 -0
- package/src/features/assistant/components/parts/__tests__/AssistantEmptyState.spec.tsx +27 -0
- package/src/features/assistant/components/parts/__tests__/AssistantSidebar.spec.tsx +58 -0
- package/src/features/assistant/components/parts/__tests__/AssistantStatusLine.spec.tsx +19 -0
- package/src/features/assistant/components/parts/__tests__/AssistantThread.spec.tsx +39 -0
- package/src/features/assistant/components/parts/__tests__/AssistantThreadHeader.spec.tsx +67 -0
- package/src/features/assistant/contexts/AssistantContext.tsx +255 -0
- package/src/features/assistant/contexts/__tests__/AssistantContext.spec.tsx +375 -0
- package/src/features/assistant/data/Assistant.ts +37 -0
- package/src/features/assistant/data/AssistantInterface.ts +11 -0
- package/src/features/assistant/data/AssistantService.ts +79 -0
- package/src/features/assistant/data/index.ts +3 -0
- package/src/features/assistant/index.ts +2 -0
- package/src/features/assistant/utils/__tests__/groupThreadsByBucket.spec.ts +24 -0
- package/src/features/assistant/utils/__tests__/resolveReferenceableModules.spec.ts +92 -0
- package/src/features/assistant/utils/groupThreadsByBucket.ts +26 -0
- package/src/features/assistant/utils/resolveReferenceableModules.ts +14 -0
- package/src/features/assistant-message/AssistantMessageModule.ts +28 -0
- package/src/features/assistant-message/components/MessageItem.tsx +60 -0
- package/src/features/assistant-message/components/MessageList.tsx +38 -0
- package/src/features/assistant-message/components/__tests__/MessageItem.spec.tsx +108 -0
- package/src/features/assistant-message/components/index.ts +2 -0
- package/src/features/assistant-message/components/parts/ReferenceBadges.tsx +46 -0
- package/src/features/assistant-message/components/parts/SuggestedFollowUps.tsx +52 -0
- package/src/features/assistant-message/components/parts/__tests__/ReferenceBadges.spec.tsx +59 -0
- package/src/features/assistant-message/components/parts/__tests__/SuggestedFollowUps.spec.tsx +29 -0
- package/src/features/assistant-message/data/AssistantMessage.ts +95 -0
- package/src/features/assistant-message/data/AssistantMessageInterface.ts +21 -0
- package/src/features/assistant-message/data/AssistantMessageService.ts +40 -0
- package/src/features/assistant-message/data/__tests__/AssistantMessage.spec.ts +158 -0
- package/src/features/assistant-message/data/index.ts +3 -0
- package/src/features/assistant-message/index.ts +2 -0
- package/src/features/user/contexts/CurrentUserContext.tsx +5 -13
- package/src/features/user/contexts/__tests__/CurrentUserContext.spec.tsx +141 -0
- package/src/index.ts +4 -0
- package/dist/HowToInterface-BKhnkzBp.d.ts +0 -17
- package/dist/HowToInterface-Cj8OuQFf.d.mts +0 -17
- package/dist/chunk-CV7UOUKQ.js.map +0 -1
- package/dist/chunk-F44ET4AC.mjs.map +0 -1
- package/dist/chunk-FKLP4NED.js.map +0 -1
- package/dist/chunk-XI35ALWY.mjs.map +0 -1
- /package/dist/{BlockNoteEditor-7HAAXN3H.mjs.map → BlockNoteEditor-JXK3JGKJ.mjs.map} +0 -0
- /package/dist/{chunk-JOJZRGZL.mjs.map → chunk-NZOUEN67.mjs.map} +0 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { MessageSquareIcon } from "lucide-react";
|
|
2
|
+
import { createJsonApiInclusion } from "../../core";
|
|
3
|
+
import { ModuleFactory } from "../../permissions";
|
|
4
|
+
import { AssistantMessage } from "./data/AssistantMessage";
|
|
5
|
+
|
|
6
|
+
export const AssistantMessageModule = (factory: ModuleFactory) =>
|
|
7
|
+
factory({
|
|
8
|
+
pageUrl: "/assistant-messages",
|
|
9
|
+
name: "assistant-messages",
|
|
10
|
+
model: AssistantMessage,
|
|
11
|
+
moduleId: "5b2e10e4-3a01-4a59-9f0f-3c4a6c6a8e11",
|
|
12
|
+
icon: MessageSquareIcon,
|
|
13
|
+
inclusions: {
|
|
14
|
+
lists: {
|
|
15
|
+
fields: [
|
|
16
|
+
createJsonApiInclusion("assistant-messages", [
|
|
17
|
+
`role`,
|
|
18
|
+
`content`,
|
|
19
|
+
`position`,
|
|
20
|
+
`suggestedQuestions`,
|
|
21
|
+
`inputTokens`,
|
|
22
|
+
`outputTokens`,
|
|
23
|
+
`references`,
|
|
24
|
+
]),
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useTranslations } from "next-intl";
|
|
4
|
+
import { Sparkles, AlertCircle } from "lucide-react";
|
|
5
|
+
import ReactMarkdown from "react-markdown";
|
|
6
|
+
import remarkGfm from "remark-gfm";
|
|
7
|
+
import type { AssistantMessageInterface } from "../data/AssistantMessageInterface";
|
|
8
|
+
import { ReferenceBadges } from "./parts/ReferenceBadges";
|
|
9
|
+
import { SuggestedFollowUps } from "./parts/SuggestedFollowUps";
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
message: AssistantMessageInterface;
|
|
13
|
+
isLatestAssistant: boolean;
|
|
14
|
+
onSelectFollowUp: (q: string) => void;
|
|
15
|
+
failedMessageIds?: Set<string>;
|
|
16
|
+
onRetry?: (tempId: string) => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function MessageItem({ message, isLatestAssistant, onSelectFollowUp, failedMessageIds, onRetry }: Props) {
|
|
20
|
+
const t = useTranslations();
|
|
21
|
+
const isUser = message.role === "user";
|
|
22
|
+
const isFailed = isUser && !!failedMessageIds?.has(message.id);
|
|
23
|
+
|
|
24
|
+
if (isUser) {
|
|
25
|
+
return (
|
|
26
|
+
<div className="flex flex-col items-end gap-1">
|
|
27
|
+
<div className="bg-primary text-primary-foreground max-w-[72%] rounded-2xl rounded-br-sm px-3.5 py-2 text-sm">
|
|
28
|
+
{message.content}
|
|
29
|
+
</div>
|
|
30
|
+
{isFailed && (
|
|
31
|
+
<div className="text-destructive flex items-center gap-2 text-xs">
|
|
32
|
+
<AlertCircle className="h-3.5 w-3.5" />
|
|
33
|
+
<span>{t("features.assistant.send_failed")}</span>
|
|
34
|
+
<button type="button" className="underline" onClick={() => onRetry?.(message.id)}>
|
|
35
|
+
{t("features.assistant.retry")}
|
|
36
|
+
</button>
|
|
37
|
+
</div>
|
|
38
|
+
)}
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className="flex max-w-[78%] flex-col gap-1.5">
|
|
45
|
+
<div className="text-muted-foreground flex items-center gap-2 pl-1 text-xs">
|
|
46
|
+
<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
|
+
<Sparkles className="h-2.5 w-2.5" />
|
|
48
|
+
</span>
|
|
49
|
+
<span>{t("features.assistant.agent_name")}</span>
|
|
50
|
+
</div>
|
|
51
|
+
<div className="bg-muted text-foreground rounded-2xl rounded-bl-sm px-3.5 py-2.5 text-sm leading-relaxed">
|
|
52
|
+
<ReactMarkdown remarkPlugins={[remarkGfm]}>{message.content}</ReactMarkdown>
|
|
53
|
+
</div>
|
|
54
|
+
<ReferenceBadges references={message.references} />
|
|
55
|
+
{isLatestAssistant && (
|
|
56
|
+
<SuggestedFollowUps questions={message.suggestedQuestions ?? []} onSelect={onSelectFollowUp} />
|
|
57
|
+
)}
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { AssistantMessageInterface } from "../data/AssistantMessageInterface";
|
|
4
|
+
import { MessageItem } from "./MessageItem";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
messages: AssistantMessageInterface[];
|
|
8
|
+
onSelectFollowUp: (q: string) => void;
|
|
9
|
+
failedMessageIds?: Set<string>;
|
|
10
|
+
onRetry?: (tempId: string) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function MessageList({ messages, onSelectFollowUp, failedMessageIds, onRetry }: Props) {
|
|
14
|
+
const ordered = [...messages].sort((a, b) => a.position - b.position);
|
|
15
|
+
|
|
16
|
+
let lastAssistantIndex = -1;
|
|
17
|
+
for (let i = ordered.length - 1; i >= 0; i--) {
|
|
18
|
+
if (ordered[i].role === "assistant") {
|
|
19
|
+
lastAssistantIndex = i;
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="flex flex-col gap-y-3">
|
|
26
|
+
{ordered.map((m, i) => (
|
|
27
|
+
<MessageItem
|
|
28
|
+
key={m.id}
|
|
29
|
+
message={m}
|
|
30
|
+
isLatestAssistant={i === lastAssistantIndex}
|
|
31
|
+
onSelectFollowUp={onSelectFollowUp}
|
|
32
|
+
failedMessageIds={failedMessageIds}
|
|
33
|
+
onRetry={onRetry}
|
|
34
|
+
/>
|
|
35
|
+
))}
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { AbstractApiData } from "../../../../core/abstracts/AbstractApiData";
|
|
4
|
+
import { DataClassRegistry } from "../../../../core/registry/DataClassRegistry";
|
|
5
|
+
import { ModuleRegistry } from "../../../../core/registry/ModuleRegistry";
|
|
6
|
+
import type { ApiRequestDataTypeInterface } from "../../../../core/interfaces/ApiRequestDataTypeInterface";
|
|
7
|
+
import type { ApiDataInterface } from "../../../../core";
|
|
8
|
+
import type { AssistantMessageInterface } from "../../data/AssistantMessageInterface";
|
|
9
|
+
import { MessageItem } from "../MessageItem";
|
|
10
|
+
|
|
11
|
+
// Test-only account model to exercise ReferenceBadges
|
|
12
|
+
class TestAccount extends AbstractApiData {
|
|
13
|
+
static identifierFields: string[] = ["name"];
|
|
14
|
+
rehydrate(data: any): this {
|
|
15
|
+
super.rehydrate(data);
|
|
16
|
+
return this;
|
|
17
|
+
}
|
|
18
|
+
createJsonApi(): any {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
const testAccountModule = {
|
|
23
|
+
name: "test-accounts",
|
|
24
|
+
pageUrl: "/accounts",
|
|
25
|
+
model: TestAccount,
|
|
26
|
+
} as unknown as ApiRequestDataTypeInterface;
|
|
27
|
+
|
|
28
|
+
beforeAll(() => {
|
|
29
|
+
DataClassRegistry.clear();
|
|
30
|
+
DataClassRegistry.registerObjectClass(testAccountModule, TestAccount);
|
|
31
|
+
ModuleRegistry.register("TestAccount" as any, testAccountModule as any);
|
|
32
|
+
});
|
|
33
|
+
afterAll(() => DataClassRegistry.clear());
|
|
34
|
+
|
|
35
|
+
function makeRehydratedAccount({ id, name }: { id: string; name: string }): ApiDataInterface {
|
|
36
|
+
const acct = new TestAccount();
|
|
37
|
+
acct.rehydrate({ jsonApi: { type: "test-accounts", id, attributes: { name } }, included: [] });
|
|
38
|
+
return acct as unknown as ApiDataInterface;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function buildMessageStub(p: {
|
|
42
|
+
role: "user" | "assistant";
|
|
43
|
+
content?: string;
|
|
44
|
+
references?: ApiDataInterface[];
|
|
45
|
+
suggestedQuestions?: string[];
|
|
46
|
+
}): AssistantMessageInterface {
|
|
47
|
+
return {
|
|
48
|
+
id: Math.random().toString(36).slice(2),
|
|
49
|
+
type: "assistant-messages",
|
|
50
|
+
role: p.role,
|
|
51
|
+
content: p.content ?? "",
|
|
52
|
+
position: 0,
|
|
53
|
+
references: p.references ?? [],
|
|
54
|
+
suggestedQuestions: p.suggestedQuestions ?? [],
|
|
55
|
+
createdAt: new Date(),
|
|
56
|
+
updatedAt: new Date(),
|
|
57
|
+
} as unknown as AssistantMessageInterface;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe("MessageItem", () => {
|
|
61
|
+
it("user message is in a right-aligned bubble with plain text", () => {
|
|
62
|
+
const msg = buildMessageStub({ role: "user", content: "hello" });
|
|
63
|
+
render(<MessageItem message={msg} isLatestAssistant={false} onSelectFollowUp={vi.fn()} />);
|
|
64
|
+
expect(screen.getByText("hello")).toBeInTheDocument();
|
|
65
|
+
// User bubble shouldn't include the assistant agent label
|
|
66
|
+
expect(screen.queryByText("features.assistant.agent_name")).not.toBeInTheDocument();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("assistant message renders bubble, references, and suggestions toggle when latest", () => {
|
|
70
|
+
const msg = buildMessageStub({
|
|
71
|
+
role: "assistant",
|
|
72
|
+
content: "**bold** reply",
|
|
73
|
+
references: [makeRehydratedAccount({ id: "acc-1", name: "Acme" })],
|
|
74
|
+
suggestedQuestions: ["follow-up 1"],
|
|
75
|
+
});
|
|
76
|
+
render(<MessageItem message={msg} isLatestAssistant={true} onSelectFollowUp={vi.fn()} />);
|
|
77
|
+
expect(screen.getByText("features.assistant.agent_name")).toBeInTheDocument();
|
|
78
|
+
expect(screen.getByRole("link", { name: /acme/i })).toBeInTheDocument();
|
|
79
|
+
expect(screen.getByRole("button", { name: /show_suggestions/ })).toBeInTheDocument();
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("assistant message NOT latest: no suggestions toggle", () => {
|
|
83
|
+
const msg = buildMessageStub({ role: "assistant", content: "prior", suggestedQuestions: ["x"] });
|
|
84
|
+
render(<MessageItem message={msg} isLatestAssistant={false} onSelectFollowUp={vi.fn()} />);
|
|
85
|
+
expect(screen.queryByRole("button", { name: /show_suggestions/ })).not.toBeInTheDocument();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("failed user message: renders retry control and calls onRetry with the id", async () => {
|
|
89
|
+
const msg = buildMessageStub({ role: "user", content: "oops" });
|
|
90
|
+
(msg as any).id = "tmp-123";
|
|
91
|
+
const onRetry = vi.fn();
|
|
92
|
+
|
|
93
|
+
render(
|
|
94
|
+
<MessageItem
|
|
95
|
+
message={msg}
|
|
96
|
+
isLatestAssistant={false}
|
|
97
|
+
onSelectFollowUp={vi.fn()}
|
|
98
|
+
failedMessageIds={new Set(["tmp-123"])}
|
|
99
|
+
onRetry={onRetry}
|
|
100
|
+
/>,
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
expect(screen.getByText("features.assistant.send_failed")).toBeInTheDocument();
|
|
104
|
+
const retryBtn = screen.getByRole("button", { name: "features.assistant.retry" });
|
|
105
|
+
retryBtn.click();
|
|
106
|
+
expect(onRetry).toHaveBeenCalledWith("tmp-123");
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import Link from "next/link";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
import type { ApiDataInterface } from "../../../../core";
|
|
6
|
+
import { usePageUrlGenerator } from "../../../../hooks";
|
|
7
|
+
import { ModuleRegistry } from "../../../../core/registry/ModuleRegistry";
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
references: ApiDataInterface[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function ReferenceBadges({ references }: Props) {
|
|
14
|
+
const t = useTranslations();
|
|
15
|
+
const generate = usePageUrlGenerator();
|
|
16
|
+
|
|
17
|
+
if (references.length === 0) return null;
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<div className="mt-2 flex flex-wrap items-center gap-2">
|
|
21
|
+
<span className="text-muted-foreground text-xs">{t("features.assistant.references_label")}</span>
|
|
22
|
+
{references.map((ref) => {
|
|
23
|
+
// ref.type is the JSON:API type string (same as module.name)
|
|
24
|
+
let module;
|
|
25
|
+
try {
|
|
26
|
+
module = ModuleRegistry.findByName(ref.type);
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const href = generate({ page: module, id: ref.id });
|
|
31
|
+
return (
|
|
32
|
+
<Link
|
|
33
|
+
key={`${ref.type}/${ref.id}`}
|
|
34
|
+
href={href}
|
|
35
|
+
target="_blank"
|
|
36
|
+
rel="noopener noreferrer"
|
|
37
|
+
className="bg-background border-border text-foreground hover:bg-accent inline-flex items-center gap-1.5 rounded-full border px-2.5 py-0.5 text-xs"
|
|
38
|
+
>
|
|
39
|
+
<span className="text-muted-foreground text-[10px]">{module.name}</span>
|
|
40
|
+
<span className="font-medium">{ref.identifier}</span>
|
|
41
|
+
</Link>
|
|
42
|
+
);
|
|
43
|
+
})}
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
import { ChevronRight, ChevronDown } from "lucide-react";
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
questions: string[];
|
|
9
|
+
onSelect: (q: string) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function SuggestedFollowUps({ questions, onSelect }: Props) {
|
|
13
|
+
const t = useTranslations();
|
|
14
|
+
const [open, setOpen] = useState(false);
|
|
15
|
+
if (questions.length === 0) return null;
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div className="mt-2">
|
|
19
|
+
<button
|
|
20
|
+
type="button"
|
|
21
|
+
onClick={() => setOpen((v) => !v)}
|
|
22
|
+
className="text-primary inline-flex items-center gap-1 text-xs font-medium"
|
|
23
|
+
>
|
|
24
|
+
{open ? (
|
|
25
|
+
<>
|
|
26
|
+
<ChevronDown className="h-3 w-3" />
|
|
27
|
+
{t("features.assistant.hide_suggestions")}
|
|
28
|
+
</>
|
|
29
|
+
) : (
|
|
30
|
+
<>
|
|
31
|
+
<ChevronRight className="h-3 w-3" />
|
|
32
|
+
{t("features.assistant.show_suggestions", { count: questions.length })}
|
|
33
|
+
</>
|
|
34
|
+
)}
|
|
35
|
+
</button>
|
|
36
|
+
{open && (
|
|
37
|
+
<div className="mt-2 flex flex-col gap-1">
|
|
38
|
+
{questions.map((q) => (
|
|
39
|
+
<button
|
|
40
|
+
key={q}
|
|
41
|
+
type="button"
|
|
42
|
+
onClick={() => onSelect(q)}
|
|
43
|
+
className="border-border bg-muted/30 hover:bg-muted rounded-md border px-3 py-1.5 text-left text-sm"
|
|
44
|
+
>
|
|
45
|
+
{q}
|
|
46
|
+
</button>
|
|
47
|
+
))}
|
|
48
|
+
</div>
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import type { ApiDataInterface } from "../../../../../core";
|
|
4
|
+
import { AbstractApiData } from "../../../../../core/abstracts/AbstractApiData";
|
|
5
|
+
import { DataClassRegistry } from "../../../../../core/registry/DataClassRegistry";
|
|
6
|
+
import { ModuleRegistry } from "../../../../../core/registry/ModuleRegistry";
|
|
7
|
+
import type { ApiRequestDataTypeInterface } from "../../../../../core/interfaces/ApiRequestDataTypeInterface";
|
|
8
|
+
import { ReferenceBadges } from "../ReferenceBadges";
|
|
9
|
+
|
|
10
|
+
class TestAccount extends AbstractApiData {
|
|
11
|
+
static identifierFields: string[] = ["name"];
|
|
12
|
+
rehydrate(data: any): this {
|
|
13
|
+
super.rehydrate(data);
|
|
14
|
+
return this;
|
|
15
|
+
}
|
|
16
|
+
createJsonApi(): any {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const testAccountModule = {
|
|
22
|
+
name: "test-accounts",
|
|
23
|
+
pageUrl: "/accounts",
|
|
24
|
+
model: TestAccount,
|
|
25
|
+
} as unknown as ApiRequestDataTypeInterface;
|
|
26
|
+
|
|
27
|
+
beforeAll(() => {
|
|
28
|
+
DataClassRegistry.clear();
|
|
29
|
+
DataClassRegistry.registerObjectClass(testAccountModule, TestAccount);
|
|
30
|
+
ModuleRegistry.register("TestAccount" as any, testAccountModule as any);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterAll(() => {
|
|
34
|
+
DataClassRegistry.clear();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
function makeRehydratedAccount({ id, name }: { id: string; name: string }): ApiDataInterface {
|
|
38
|
+
const acct = new TestAccount();
|
|
39
|
+
acct.rehydrate({
|
|
40
|
+
jsonApi: { type: "test-accounts", id, attributes: { name } },
|
|
41
|
+
included: [],
|
|
42
|
+
});
|
|
43
|
+
return acct as unknown as ApiDataInterface;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe("ReferenceBadges", () => {
|
|
47
|
+
it("renders a chip per reference with type label + identifier", () => {
|
|
48
|
+
const references = [makeRehydratedAccount({ id: "acc-1", name: "Acme" })];
|
|
49
|
+
render(<ReferenceBadges references={references} />);
|
|
50
|
+
const link = screen.getByRole("link", { name: /acme/i });
|
|
51
|
+
expect(link).toBeInTheDocument();
|
|
52
|
+
expect(link.getAttribute("href")).toContain("/accounts/acc-1");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("renders nothing for empty references", () => {
|
|
56
|
+
const { container } = render(<ReferenceBadges references={[]} />);
|
|
57
|
+
expect(container.firstChild).toBeNull();
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import userEvent from "@testing-library/user-event";
|
|
4
|
+
import { SuggestedFollowUps } from "../SuggestedFollowUps";
|
|
5
|
+
|
|
6
|
+
describe("SuggestedFollowUps", () => {
|
|
7
|
+
it("is collapsed by default; toggling reveals the buttons", async () => {
|
|
8
|
+
render(<SuggestedFollowUps questions={["q1", "q2", "q3"]} onSelect={vi.fn()} />);
|
|
9
|
+
expect(screen.queryByRole("button", { name: "q1" })).not.toBeInTheDocument();
|
|
10
|
+
const toggle = screen.getByRole("button", { name: /show_suggestions/ });
|
|
11
|
+
await userEvent.click(toggle);
|
|
12
|
+
expect(screen.getByRole("button", { name: "q1" })).toBeInTheDocument();
|
|
13
|
+
expect(screen.getByRole("button", { name: "q2" })).toBeInTheDocument();
|
|
14
|
+
expect(screen.getByRole("button", { name: "q3" })).toBeInTheDocument();
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("clicking a question calls onSelect immediately", async () => {
|
|
18
|
+
const onSelect = vi.fn();
|
|
19
|
+
render(<SuggestedFollowUps questions={["q1"]} onSelect={onSelect} />);
|
|
20
|
+
await userEvent.click(screen.getByRole("button", { name: /show_suggestions/ }));
|
|
21
|
+
await userEvent.click(screen.getByRole("button", { name: "q1" }));
|
|
22
|
+
expect(onSelect).toHaveBeenCalledWith("q1");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("renders nothing for empty list", () => {
|
|
26
|
+
const { container } = render(<SuggestedFollowUps questions={[]} onSelect={vi.fn()} />);
|
|
27
|
+
expect(container.firstChild).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { AbstractApiData, ApiDataInterface, JsonApiHydratedDataInterface, Modules } from "../../../core";
|
|
2
|
+
import { AssistantMessageInput, AssistantMessageInterface, AssistantMessageRole } from "./AssistantMessageInterface";
|
|
3
|
+
import { resolveReferenceableModules } from "../../assistant/utils/resolveReferenceableModules";
|
|
4
|
+
|
|
5
|
+
export class AssistantMessage extends AbstractApiData implements AssistantMessageInterface {
|
|
6
|
+
private _role?: AssistantMessageRole;
|
|
7
|
+
private _content?: string;
|
|
8
|
+
private _position?: number;
|
|
9
|
+
private _suggestedQuestions?: string[];
|
|
10
|
+
private _inputTokens?: number;
|
|
11
|
+
private _outputTokens?: number;
|
|
12
|
+
private _references?: ApiDataInterface[];
|
|
13
|
+
|
|
14
|
+
get role(): AssistantMessageRole {
|
|
15
|
+
return this._role ?? "assistant";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get content(): string {
|
|
19
|
+
return this._content ?? "";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
get position(): number {
|
|
23
|
+
return this._position ?? 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get suggestedQuestions(): string[] {
|
|
27
|
+
return this._suggestedQuestions ?? [];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
get inputTokens(): number | undefined {
|
|
31
|
+
return this._inputTokens;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get outputTokens(): number | undefined {
|
|
35
|
+
return this._outputTokens;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
get references(): ApiDataInterface[] {
|
|
39
|
+
return this._references ?? [];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
rehydrate(data: JsonApiHydratedDataInterface): this {
|
|
43
|
+
super.rehydrate(data);
|
|
44
|
+
const attrs = data.jsonApi.attributes ?? {};
|
|
45
|
+
this._role = attrs.role as AssistantMessageRole | undefined;
|
|
46
|
+
this._content = attrs.content;
|
|
47
|
+
this._position = typeof attrs.position === "number" ? attrs.position : Number(attrs.position ?? 0);
|
|
48
|
+
this._suggestedQuestions = Array.isArray(attrs.suggestedQuestions) ? attrs.suggestedQuestions : [];
|
|
49
|
+
this._inputTokens = attrs.inputTokens;
|
|
50
|
+
this._outputTokens = attrs.outputTokens;
|
|
51
|
+
const refs = this._readIncludedPolymorphic<ApiDataInterface>(data, "references", resolveReferenceableModules());
|
|
52
|
+
this._references = Array.isArray(refs) ? refs : refs ? [refs] : [];
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
createJsonApi(data: AssistantMessageInput) {
|
|
57
|
+
return {
|
|
58
|
+
data: {
|
|
59
|
+
type: Modules.AssistantMessage.name,
|
|
60
|
+
id: data.id,
|
|
61
|
+
attributes: {
|
|
62
|
+
role: data.role,
|
|
63
|
+
content: data.content,
|
|
64
|
+
position: data.position,
|
|
65
|
+
},
|
|
66
|
+
relationships: {
|
|
67
|
+
assistant: {
|
|
68
|
+
data: { type: Modules.Assistant.name, id: data.assistantId },
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
included: [],
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
static buildOptimistic(params: { content: string; position: number; assistantId?: string }): AssistantMessage {
|
|
77
|
+
const msg = new AssistantMessage();
|
|
78
|
+
const jsonApi: Record<string, unknown> = {
|
|
79
|
+
id: `tmp-${crypto.randomUUID()}`,
|
|
80
|
+
type: Modules.AssistantMessage.name,
|
|
81
|
+
attributes: {
|
|
82
|
+
role: "user",
|
|
83
|
+
content: params.content,
|
|
84
|
+
position: params.position,
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
if (params.assistantId) {
|
|
88
|
+
jsonApi.relationships = {
|
|
89
|
+
assistant: { data: { type: Modules.Assistant.name, id: params.assistantId } },
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
msg.rehydrate({ jsonApi: jsonApi as any, included: [] });
|
|
93
|
+
return msg;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ApiDataInterface } from "../../../core";
|
|
2
|
+
|
|
3
|
+
export type AssistantMessageRole = "user" | "assistant" | "system";
|
|
4
|
+
|
|
5
|
+
export type AssistantMessageInput = {
|
|
6
|
+
id: string;
|
|
7
|
+
role: AssistantMessageRole;
|
|
8
|
+
content: string;
|
|
9
|
+
position: number;
|
|
10
|
+
assistantId: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export interface AssistantMessageInterface extends ApiDataInterface {
|
|
14
|
+
get role(): AssistantMessageRole;
|
|
15
|
+
get content(): string;
|
|
16
|
+
get position(): number;
|
|
17
|
+
get suggestedQuestions(): string[];
|
|
18
|
+
get inputTokens(): number | undefined;
|
|
19
|
+
get outputTokens(): number | undefined;
|
|
20
|
+
get references(): ApiDataInterface[];
|
|
21
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { AbstractService, EndpointCreator, HttpMethod, Modules, NextRef } from "../../../core";
|
|
2
|
+
import { AssistantMessageInterface } from "./AssistantMessageInterface";
|
|
3
|
+
|
|
4
|
+
export class AssistantMessageService extends AbstractService {
|
|
5
|
+
static async findByAssistant(params: { assistantId: string; next?: NextRef }): Promise<AssistantMessageInterface[]> {
|
|
6
|
+
const endpoint = new EndpointCreator({
|
|
7
|
+
endpoint: Modules.Assistant,
|
|
8
|
+
id: params.assistantId,
|
|
9
|
+
childEndpoint: Modules.AssistantMessage,
|
|
10
|
+
});
|
|
11
|
+
return this.callApi({
|
|
12
|
+
type: Modules.AssistantMessage,
|
|
13
|
+
method: HttpMethod.GET,
|
|
14
|
+
endpoint: endpoint.generate(),
|
|
15
|
+
next: params.next,
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static async findOne(params: { id: string }): Promise<AssistantMessageInterface> {
|
|
20
|
+
return this.callApi<AssistantMessageInterface>({
|
|
21
|
+
type: Modules.AssistantMessage,
|
|
22
|
+
method: HttpMethod.GET,
|
|
23
|
+
endpoint: new EndpointCreator({
|
|
24
|
+
endpoint: Modules.AssistantMessage,
|
|
25
|
+
id: params.id,
|
|
26
|
+
}).generate(),
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static async delete(params: { id: string }): Promise<void> {
|
|
31
|
+
await this.callApi({
|
|
32
|
+
type: Modules.AssistantMessage,
|
|
33
|
+
method: HttpMethod.DELETE,
|
|
34
|
+
endpoint: new EndpointCreator({
|
|
35
|
+
endpoint: Modules.AssistantMessage,
|
|
36
|
+
id: params.id,
|
|
37
|
+
}).generate(),
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|