@carlonicora/nextjs-jsonapi 2.0.3 → 2.1.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-DMh-zApr.d.mts +34 -0
- package/dist/AssistantInterface-DwdBzS9f.d.ts +34 -0
- package/dist/{BlockNoteEditor-QMZLV62B.js → BlockNoteEditor-C5CYCS3I.js} +22 -20
- package/dist/BlockNoteEditor-C5CYCS3I.js.map +1 -0
- package/dist/{BlockNoteEditor-QGEOVS3T.mjs → BlockNoteEditor-NXDUGMP7.mjs} +7 -5
- package/dist/BlockNoteEditor-NXDUGMP7.mjs.map +1 -0
- package/dist/{BlockNoteViewer-UW5VZ5UF.js → BlockNoteViewer-6N2GQWTI.js} +4 -2
- package/dist/BlockNoteViewer-6N2GQWTI.js.map +1 -0
- package/dist/{BlockNoteViewer-OPSCTHKJ.mjs → BlockNoteViewer-IBQRJ3WM.mjs} +4 -2
- package/dist/BlockNoteViewer-IBQRJ3WM.mjs.map +1 -0
- package/dist/billing/index.js +357 -357
- package/dist/billing/index.mjs +3 -3
- package/dist/{chunk-DS4IBFWA.js → chunk-6YIZX26T.js} +185 -33
- package/dist/chunk-6YIZX26T.js.map +1 -0
- package/dist/{chunk-QHGSBZCL.mjs → chunk-A7AGXKZX.mjs} +1094 -732
- package/dist/chunk-A7AGXKZX.mjs.map +1 -0
- package/dist/{chunk-PBKZYJRJ.js → chunk-I3GHIU4Y.js} +1254 -892
- package/dist/chunk-I3GHIU4Y.js.map +1 -0
- package/dist/{chunk-UXJAS6C5.js → chunk-JG7MCYCW.js} +7 -7
- package/dist/{chunk-UXJAS6C5.js.map → chunk-JG7MCYCW.js.map} +1 -1
- package/dist/{chunk-HN5ED7OF.mjs → chunk-MAWTFEY3.mjs} +2 -2
- package/dist/{chunk-BIL2WQEV.mjs → chunk-P5TMD7GK.mjs} +158 -6
- package/dist/chunk-P5TMD7GK.mjs.map +1 -0
- package/dist/client/index.js +4 -4
- package/dist/client/index.mjs +3 -3
- package/dist/components/index.d.mts +102 -11
- package/dist/components/index.d.ts +102 -11
- package/dist/components/index.js +12 -4
- package/dist/components/index.js.map +1 -1
- package/dist/components/index.mjs +11 -3
- package/dist/contexts/index.d.mts +29 -3
- package/dist/contexts/index.d.ts +29 -3
- package/dist/contexts/index.js +4 -4
- package/dist/contexts/index.mjs +3 -3
- package/dist/core/index.d.mts +99 -2
- package/dist/core/index.d.ts +99 -2
- package/dist/core/index.js +8 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +7 -1
- package/dist/features/help/index.js +37 -37
- package/dist/features/help/index.mjs +3 -3
- package/dist/features/tokenusage/index.js +71 -71
- package/dist/features/tokenusage/index.mjs +3 -3
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +9 -3
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +8 -2
- package/dist/server/index.js +3 -3
- package/dist/server/index.mjs +1 -1
- package/package.json +1 -1
- package/src/components/editors/BlockNoteEditor.tsx +5 -1
- package/src/components/editors/BlockNoteEditorSuggestionMenuController.tsx +14 -2
- package/src/components/index.ts +1 -0
- package/src/components/viewers/BlockNoteViewer.tsx +5 -1
- package/src/core/index.ts +2 -0
- package/src/core/registry/ModuleRegistry.ts +1 -0
- package/src/features/assistant/components/containers/AssistantPageContainer.tsx +123 -0
- package/src/features/assistant/components/index.ts +2 -0
- package/src/features/assistant/components/parts/AssistantBlockNoteComposer.tsx +135 -0
- package/src/features/assistant/components/parts/AssistantThread.tsx +4 -1
- package/src/features/assistant/contexts/AssistantContext.tsx +60 -13
- package/src/features/assistant/contexts/__tests__/AssistantContext.spec.tsx +81 -0
- package/src/features/assistant/data/Assistant.ts +29 -1
- package/src/features/assistant/data/AssistantInterface.ts +15 -0
- package/src/features/assistant/data/AssistantService.ts +19 -2
- package/src/features/assistant/data/__tests__/Assistant.scope.spec.ts +64 -0
- package/src/features/assistant-action/AssistantActionModule.ts +13 -0
- package/src/features/assistant-action/components/ApprovalActionCard.tsx +127 -0
- package/src/features/assistant-action/components/AssistantContainerWithApprovals.tsx +34 -0
- package/src/features/assistant-action/components/index.ts +2 -0
- package/src/features/assistant-action/data/AssistantAction.ts +58 -0
- package/src/features/assistant-action/data/AssistantActionInterface.ts +24 -0
- package/src/features/assistant-action/data/AssistantActionService.ts +51 -0
- package/src/features/assistant-action/data/__tests__/AssistantAction.spec.ts +171 -0
- package/src/features/assistant-action/data/index.ts +3 -0
- package/src/features/assistant-action/index.ts +2 -0
- package/src/features/assistant-message/components/MessageItem.tsx +57 -6
- package/src/features/assistant-message/components/MessageList.tsx +11 -2
- package/src/features/assistant-message/components/__tests__/MessageItem.mentions.spec.tsx +34 -0
- package/src/features/assistant-message/components/parts/MessageSourcesContainer.tsx +30 -13
- package/src/features/assistant-message/components/parts/tabs/CitationsTab.tsx +23 -5
- package/src/features/assistant-message/components/parts/tabs/ContentsTab.tsx +3 -1
- package/src/features/assistant-message/components/parts/tabs/ReferencesTab.tsx +4 -1
- package/src/features/assistant-message/components/parts/tabs/useEntityLabel.ts +35 -0
- package/src/features/assistant-message/data/AssistantMessage.ts +11 -2
- package/src/features/index.ts +1 -0
- package/dist/AssistantInterface-B56qznuH.d.mts +0 -16
- package/dist/AssistantInterface-CHiJIYDY.d.ts +0 -16
- package/dist/BlockNoteEditor-QGEOVS3T.mjs.map +0 -1
- package/dist/BlockNoteEditor-QMZLV62B.js.map +0 -1
- package/dist/BlockNoteViewer-OPSCTHKJ.mjs.map +0 -1
- package/dist/BlockNoteViewer-UW5VZ5UF.js.map +0 -1
- package/dist/chunk-BIL2WQEV.mjs.map +0 -1
- package/dist/chunk-DS4IBFWA.js.map +0 -1
- package/dist/chunk-PBKZYJRJ.js.map +0 -1
- package/dist/chunk-QHGSBZCL.mjs.map +0 -1
- /package/dist/{chunk-HN5ED7OF.mjs.map → chunk-MAWTFEY3.mjs.map} +0 -0
package/dist/server/index.js
CHANGED
|
@@ -15,7 +15,7 @@ var _chunkIOLU4ASDjs = require('../chunk-IOLU4ASD.js');
|
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
|
|
18
|
-
var
|
|
18
|
+
var _chunk6YIZX26Tjs = require('../chunk-6YIZX26T.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(_pako.ungzip.call(void 0, Buffer.from(rawModules, "base64"), { toText: true }));
|
|
88
88
|
const selectedModule = modules.find((module) => module.id === params.module.moduleId);
|
|
89
|
-
return
|
|
89
|
+
return _chunk6YIZX26Tjs.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 = _chunk6YIZX26Tjs.AuthService; exports.ServerCompanyService = _chunk6YIZX26Tjs.CompanyService; exports.ServerContentService = _chunk6YIZX26Tjs.ContentService; exports.ServerFeatureService = _chunk6YIZX26Tjs.FeatureService; exports.ServerJsonApiDelete = ServerJsonApiDelete; exports.ServerJsonApiGet = ServerJsonApiGet; exports.ServerJsonApiPatch = ServerJsonApiPatch; exports.ServerJsonApiPost = ServerJsonApiPost; exports.ServerJsonApiPut = ServerJsonApiPut; exports.ServerNotificationService = _chunk6YIZX26Tjs.NotificationService; exports.ServerPushService = _chunk6YIZX26Tjs.PushService; exports.ServerRoleService = _chunk6YIZX26Tjs.RoleService; exports.ServerS3Service = _chunk6YIZX26Tjs.S3Service; exports.ServerSession = ServerSession; exports.ServerUserService = _chunk6YIZX26Tjs.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 = _chunkIOLU4ASDjs.serverRequest;
|
|
300
300
|
//# sourceMappingURL=index.js.map
|
package/dist/server/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
XIcon,
|
|
37
37
|
} from "lucide-react";
|
|
38
38
|
import { useTranslations } from "next-intl";
|
|
39
|
+
import { useTheme } from "next-themes";
|
|
39
40
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
40
41
|
import { getPublicApiUrl } from "../../client/config";
|
|
41
42
|
import { getClientToken } from "../../client/token";
|
|
@@ -410,6 +411,7 @@ export default function BlockNoteEditor({
|
|
|
410
411
|
}: BlockNoteEditorProps): React.JSX.Element {
|
|
411
412
|
const t = useTranslations();
|
|
412
413
|
const locale = useI18nLocale();
|
|
414
|
+
const { resolvedTheme } = useTheme();
|
|
413
415
|
const { company } = useCurrentUserContext<UserInterface>();
|
|
414
416
|
|
|
415
417
|
const [acceptedChanges, setAcceptedChanges] = useState<Set<string>>(new Set());
|
|
@@ -833,7 +835,9 @@ export default function BlockNoteEditor({
|
|
|
833
835
|
editable={onChange !== undefined}
|
|
834
836
|
formattingToolbar={false}
|
|
835
837
|
slashMenu={!aiConfig}
|
|
836
|
-
theme
|
|
838
|
+
// Follow the app theme: hardcoding "light" leaves dark (light-theme)
|
|
839
|
+
// text on the dark card, which is nearly unreadable in dark mode.
|
|
840
|
+
theme={resolvedTheme === "dark" ? "dark" : "light"}
|
|
837
841
|
// `className` is applied by BlockNote to both the main `.bn-container`
|
|
838
842
|
// AND `editor.portalElement` (the floating-UI portal root). Gate `p-4`
|
|
839
843
|
// on `.bn-container` so it doesn't add padding to the empty portal
|
|
@@ -12,6 +12,16 @@ export interface MentionItem {
|
|
|
12
12
|
icon?: React.ReactNode;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/**
|
|
16
|
+
* A suggestion item enriched with the entity it points at. Entity NAMES are not
|
|
17
|
+
* unique (two clues can both be called "New clue"), so a custom
|
|
18
|
+
* `suggestionMenuComponent` MUST key its rows on `id`, never on `title`.
|
|
19
|
+
*/
|
|
20
|
+
export type MentionSuggestionItem = DefaultReactSuggestionItem & {
|
|
21
|
+
id?: string;
|
|
22
|
+
entityType?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
15
25
|
export type MentionSearchFn = (query: string, params?: Record<string, string>) => Promise<MentionItem[]>;
|
|
16
26
|
|
|
17
27
|
export type MentionInsertFn = (id: string, name: string, entityType: string) => void;
|
|
@@ -57,7 +67,9 @@ export function BlockNoteEditorMentionSuggestionMenu({
|
|
|
57
67
|
const getItems = useCallback(
|
|
58
68
|
async (query: string): Promise<DefaultReactSuggestionItem[]> => {
|
|
59
69
|
const results = await mentionSearchFn(query, mentionSearchParams);
|
|
60
|
-
const items:
|
|
70
|
+
const items: MentionSuggestionItem[] = results.map((item) => ({
|
|
71
|
+
id: item.id,
|
|
72
|
+
entityType: item.entityType,
|
|
61
73
|
title: item.name,
|
|
62
74
|
subtext: item.entityType,
|
|
63
75
|
icon: item.icon as React.ReactElement | undefined,
|
|
@@ -76,7 +88,7 @@ export function BlockNoteEditorMentionSuggestionMenu({
|
|
|
76
88
|
},
|
|
77
89
|
}));
|
|
78
90
|
if (items.length === 0) {
|
|
79
|
-
items.push({ title: KEEP_OPEN_SENTINEL_TITLE, onItemClick: () => {} });
|
|
91
|
+
items.push({ id: KEEP_OPEN_SENTINEL_TITLE, title: KEEP_OPEN_SENTINEL_TITLE, onItemClick: () => {} });
|
|
80
92
|
}
|
|
81
93
|
return items;
|
|
82
94
|
},
|
package/src/components/index.ts
CHANGED
|
@@ -26,6 +26,7 @@ export * from "../features/content/components";
|
|
|
26
26
|
export * from "../features/how-to/components";
|
|
27
27
|
export * from "../features/assistant/components";
|
|
28
28
|
export * from "../features/assistant-message/components";
|
|
29
|
+
export * from "../features/assistant-action/components";
|
|
29
30
|
export * from "../features/feature/components";
|
|
30
31
|
export * from "../features/notification/components";
|
|
31
32
|
export * from "../features/onboarding/components";
|
|
@@ -7,6 +7,7 @@ import type { PartialBlock } from "@blocknote/core";
|
|
|
7
7
|
// this can render on auth-free public pages.
|
|
8
8
|
import { BlockNoteView } from "@blocknote/shadcn";
|
|
9
9
|
import "@blocknote/shadcn/style.css";
|
|
10
|
+
import { useTheme } from "next-themes";
|
|
10
11
|
|
|
11
12
|
function normalize(content: unknown): PartialBlock[] | undefined {
|
|
12
13
|
if (Array.isArray(content) && content.length > 0) return content as PartialBlock[];
|
|
@@ -23,5 +24,8 @@ function normalize(content: unknown): PartialBlock[] | undefined {
|
|
|
23
24
|
|
|
24
25
|
export function BlockNoteViewer({ content }: { content: unknown }) {
|
|
25
26
|
const editor = useCreateBlockNote({ initialContent: normalize(content) });
|
|
26
|
-
|
|
27
|
+
// Outside a next-themes ThemeProvider (public pages) resolvedTheme is
|
|
28
|
+
// undefined, so this falls back to "light".
|
|
29
|
+
const { resolvedTheme } = useTheme();
|
|
30
|
+
return <BlockNoteView editor={editor} editable={false} theme={resolvedTheme === "dark" ? "dark" : "light"} />;
|
|
27
31
|
}
|
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/assistant-action/AssistantActionModule";
|
|
69
|
+
export * from "../features/assistant-action/data";
|
|
68
70
|
export * from "../features/chunk/ChunkModule";
|
|
69
71
|
export * from "../features/chunk/data";
|
|
70
72
|
export * from "../features/feature/data";
|
|
@@ -18,6 +18,7 @@ export interface FoundationModuleDefinitions {
|
|
|
18
18
|
HowTo: ModuleWithPermissions;
|
|
19
19
|
Assistant: ModuleWithPermissions;
|
|
20
20
|
AssistantMessage: ModuleWithPermissions;
|
|
21
|
+
AssistantAction: ModuleWithPermissions;
|
|
21
22
|
Chunk: ModuleWithPermissions;
|
|
22
23
|
// Billing modules - READ: all users, UPDATE: CompanyAdministrator, ADMIN: Administrator
|
|
23
24
|
Billing: ModuleWithPermissions;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { DefaultReactSuggestionItem, SuggestionMenuProps } from "@blocknote/react";
|
|
4
|
+
import { Sparkles } from "lucide-react";
|
|
5
|
+
import { useTranslations } from "next-intl";
|
|
6
|
+
import type React from "react";
|
|
7
|
+
import { RoundPageContainer } from "../../../../components/containers/RoundPageContainer";
|
|
8
|
+
import type { MentionResolveFn } from "../../../../components/editors/BlockNoteEditorMentionInlineContent";
|
|
9
|
+
import type { MentionItem } from "../../../../components/editors/BlockNoteEditorSuggestionMenuController";
|
|
10
|
+
import { SectionHeader } from "../../../../components/typography";
|
|
11
|
+
import { Modules } from "../../../../core";
|
|
12
|
+
import type { ApprovalActionRenderer, MentionRenderer } from "../../../assistant-message/components/MessageItem";
|
|
13
|
+
import { useAssistantContext } from "../../contexts/AssistantContext";
|
|
14
|
+
import { AssistantBlockNoteComposer } from "../parts/AssistantBlockNoteComposer";
|
|
15
|
+
import { AssistantSidebar } from "../parts/AssistantSidebar";
|
|
16
|
+
import { AssistantThread } from "../parts/AssistantThread";
|
|
17
|
+
import { AssistantThreadHeader } from "../parts/AssistantThreadHeader";
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
renderApprovalAction?: ApprovalActionRenderer;
|
|
21
|
+
renderMention?: MentionRenderer;
|
|
22
|
+
mentionSearchFn?: (query: string, params?: Record<string, string>) => Promise<MentionItem[]>;
|
|
23
|
+
mentionSearchParams?: Record<string, string>;
|
|
24
|
+
mentionResolveFn?: MentionResolveFn;
|
|
25
|
+
/**
|
|
26
|
+
* Custom mention menu for the composer. Without one BlockNote renders its
|
|
27
|
+
* default menu, which keys rows on the item title — duplicate entity names
|
|
28
|
+
* then collide.
|
|
29
|
+
*/
|
|
30
|
+
suggestionMenuComponent?: React.FC<SuggestionMenuProps<DefaultReactSuggestionItem>>;
|
|
31
|
+
/** Rendered inside the sidebar's details panel above the thread list. */
|
|
32
|
+
detailsTitle: string;
|
|
33
|
+
detailsIcon: React.ReactNode;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Full-page assistant: the thread list lives in the page's details panel and
|
|
38
|
+
* the conversation fills the card. Unlike `AssistantContainer` the composer is
|
|
39
|
+
* the BlockNote one, so the very first message of a thread can carry mentions.
|
|
40
|
+
*/
|
|
41
|
+
export function AssistantPageContainer({
|
|
42
|
+
renderApprovalAction,
|
|
43
|
+
renderMention,
|
|
44
|
+
mentionSearchFn,
|
|
45
|
+
mentionSearchParams,
|
|
46
|
+
mentionResolveFn,
|
|
47
|
+
suggestionMenuComponent,
|
|
48
|
+
detailsTitle,
|
|
49
|
+
detailsIcon,
|
|
50
|
+
}: Props) {
|
|
51
|
+
const t = useTranslations();
|
|
52
|
+
const ctx = useAssistantContext();
|
|
53
|
+
const showThread = !!ctx.assistant || ctx.sending || ctx.messages.length > 0;
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<RoundPageContainer
|
|
57
|
+
module={Modules.Assistant}
|
|
58
|
+
fullWidth
|
|
59
|
+
forceHeader
|
|
60
|
+
defaultDetailsOpen
|
|
61
|
+
detailsTitle={detailsTitle}
|
|
62
|
+
detailsIcon={detailsIcon}
|
|
63
|
+
details={
|
|
64
|
+
// The panel supplies its own padding and border; cancel both so the
|
|
65
|
+
// sidebar reads as the panel body rather than a card inside it.
|
|
66
|
+
<div className="-m-4 flex h-full min-h-0 [&>aside]:w-full [&>aside]:border-r-0 [&>aside]:bg-transparent">
|
|
67
|
+
<AssistantSidebar
|
|
68
|
+
threads={ctx.threads}
|
|
69
|
+
activeId={ctx.assistant?.id}
|
|
70
|
+
onSelect={ctx.selectThread}
|
|
71
|
+
onNew={ctx.startNew}
|
|
72
|
+
/>
|
|
73
|
+
</div>
|
|
74
|
+
}
|
|
75
|
+
>
|
|
76
|
+
<div className="bg-background flex h-full w-full flex-col overflow-hidden rounded-lg border">
|
|
77
|
+
{showThread ? (
|
|
78
|
+
<>
|
|
79
|
+
{ctx.assistant ? (
|
|
80
|
+
<AssistantThreadHeader
|
|
81
|
+
assistant={ctx.assistant}
|
|
82
|
+
onRename={(title) => ctx.renameThread(ctx.assistant!.id, title)}
|
|
83
|
+
onDelete={() => ctx.deleteThread(ctx.assistant!.id)}
|
|
84
|
+
/>
|
|
85
|
+
) : (
|
|
86
|
+
<div className="flex items-center justify-between border-b px-5 py-3" aria-hidden>
|
|
87
|
+
<div className="h-5" />
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
<AssistantThread
|
|
91
|
+
messages={ctx.messages}
|
|
92
|
+
sending={ctx.sending}
|
|
93
|
+
status={ctx.status}
|
|
94
|
+
onSelectFollowUp={ctx.sendMessage}
|
|
95
|
+
failedMessageIds={ctx.failedMessageIds}
|
|
96
|
+
onRetry={ctx.retrySend}
|
|
97
|
+
renderApprovalAction={renderApprovalAction}
|
|
98
|
+
renderMention={renderMention}
|
|
99
|
+
/>
|
|
100
|
+
</>
|
|
101
|
+
) : (
|
|
102
|
+
<div className="flex flex-1 items-center justify-center p-10">
|
|
103
|
+
<div className="flex max-w-2xl flex-col items-center text-center">
|
|
104
|
+
<span className="mb-3 flex h-10 w-10 items-center justify-center rounded-full bg-gradient-to-br from-blue-400 to-violet-500 text-white">
|
|
105
|
+
<Sparkles className="h-5 w-5" />
|
|
106
|
+
</span>
|
|
107
|
+
<SectionHeader>{t("features.assistant.empty_state.title")}</SectionHeader>
|
|
108
|
+
<p className="text-muted-foreground mt-1 text-sm">{t("features.assistant.empty_state.subtitle")}</p>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
)}
|
|
112
|
+
<AssistantBlockNoteComposer
|
|
113
|
+
onSend={(content, opts) => ctx.sendMessage(content, { contentBlocks: opts.contentBlocks })}
|
|
114
|
+
disabled={ctx.sending}
|
|
115
|
+
mentionSearchFn={mentionSearchFn}
|
|
116
|
+
mentionSearchParams={mentionSearchParams}
|
|
117
|
+
mentionResolveFn={mentionResolveFn}
|
|
118
|
+
suggestionMenuComponent={suggestionMenuComponent}
|
|
119
|
+
/>
|
|
120
|
+
</div>
|
|
121
|
+
</RoundPageContainer>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ArrowUp } from "lucide-react";
|
|
4
|
+
import { useTranslations } from "next-intl";
|
|
5
|
+
import { useCallback, useId, useState, type FC, type KeyboardEvent } from "react";
|
|
6
|
+
import type { DefaultReactSuggestionItem, SuggestionMenuProps } from "@blocknote/react";
|
|
7
|
+
import { BlockNoteEditorContainer } from "../../../../components/editors/BlockNoteEditorContainer";
|
|
8
|
+
import type { MentionResolveFn } from "../../../../components/editors/BlockNoteEditorMentionInlineContent";
|
|
9
|
+
import type { MentionItem } from "../../../../components/editors/BlockNoteEditorSuggestionMenuController";
|
|
10
|
+
import { Button } from "../../../../shadcnui";
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
onSend: (content: string, opts: { contentBlocks: unknown[] }) => Promise<void>;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
mentionSearchFn?: (query: string, params?: Record<string, string>) => Promise<MentionItem[]>;
|
|
16
|
+
mentionSearchParams?: Record<string, string>;
|
|
17
|
+
mentionResolveFn?: MentionResolveFn;
|
|
18
|
+
/**
|
|
19
|
+
* Custom mention menu. Without one BlockNote renders its default menu, which
|
|
20
|
+
* keys rows on the item title — duplicate entity names then collide.
|
|
21
|
+
*/
|
|
22
|
+
suggestionMenuComponent?: FC<SuggestionMenuProps<DefaultReactSuggestionItem>>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Flattens a BlockNote document to a single plain-text string. This is only a
|
|
27
|
+
* FALLBACK for the server, which re-derives the stored text from the blocks it
|
|
28
|
+
* receives — the blocks are the source of truth, never this string.
|
|
29
|
+
*/
|
|
30
|
+
function flattenBlocks(blocks: unknown[]): string {
|
|
31
|
+
const lines: string[] = [];
|
|
32
|
+
|
|
33
|
+
const readInline = (inline: unknown): string => {
|
|
34
|
+
if (typeof inline === "string") return inline;
|
|
35
|
+
if (!inline || typeof inline !== "object") return "";
|
|
36
|
+
const node = inline as { type?: string; text?: string; props?: { alias?: string }; content?: unknown };
|
|
37
|
+
if (node.type === "mention") return node.props?.alias ?? "";
|
|
38
|
+
if (typeof node.text === "string") return node.text;
|
|
39
|
+
if (Array.isArray(node.content)) return node.content.map(readInline).join("");
|
|
40
|
+
return "";
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const readBlock = (block: unknown): void => {
|
|
44
|
+
if (!block || typeof block !== "object") return;
|
|
45
|
+
const node = block as { content?: unknown; children?: unknown };
|
|
46
|
+
if (Array.isArray(node.content)) {
|
|
47
|
+
const line = node.content.map(readInline).join("").trim();
|
|
48
|
+
if (line) lines.push(line);
|
|
49
|
+
} else if (typeof node.content === "string" && node.content.trim()) {
|
|
50
|
+
lines.push(node.content.trim());
|
|
51
|
+
}
|
|
52
|
+
if (Array.isArray(node.children)) node.children.forEach(readBlock);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
blocks.forEach(readBlock);
|
|
56
|
+
return lines.join("\n");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Mention-capable chat composer. Unlike `AssistantComposer` (plain textarea) it
|
|
61
|
+
* sends the BlockNote document alongside the flattened text, so the server can
|
|
62
|
+
* resolve `@`-mentions to real entities and pin them into the agent's focus.
|
|
63
|
+
*/
|
|
64
|
+
export function AssistantBlockNoteComposer({
|
|
65
|
+
onSend,
|
|
66
|
+
disabled,
|
|
67
|
+
mentionSearchFn,
|
|
68
|
+
mentionSearchParams,
|
|
69
|
+
mentionResolveFn,
|
|
70
|
+
suggestionMenuComponent,
|
|
71
|
+
}: Props) {
|
|
72
|
+
const t = useTranslations();
|
|
73
|
+
// React's generated ids carry delimiters (`:r0:` / `«r0»`) and this value ends
|
|
74
|
+
// up inside an S3 object key, so keep it to path-safe characters.
|
|
75
|
+
const editorId = useId().replace(/[^a-zA-Z0-9]/g, "");
|
|
76
|
+
const [blocks, setBlocks] = useState<unknown[]>([]);
|
|
77
|
+
const [isEmpty, setIsEmpty] = useState(true);
|
|
78
|
+
// Remounting the editor is the only reliable way to clear it: the editor
|
|
79
|
+
// ignores an `initialContent` that merely echoes what it last emitted.
|
|
80
|
+
const [resetKey, setResetKey] = useState(0);
|
|
81
|
+
|
|
82
|
+
const canSend = !isEmpty && !disabled;
|
|
83
|
+
|
|
84
|
+
const submit = useCallback(async () => {
|
|
85
|
+
if (isEmpty || disabled) return;
|
|
86
|
+
const payload = blocks;
|
|
87
|
+
const plainText = flattenBlocks(payload);
|
|
88
|
+
setBlocks([]);
|
|
89
|
+
setIsEmpty(true);
|
|
90
|
+
setResetKey((previous) => previous + 1);
|
|
91
|
+
await onSend(plainText, { contentBlocks: payload });
|
|
92
|
+
}, [blocks, disabled, isEmpty, onSend]);
|
|
93
|
+
|
|
94
|
+
const handleKeyDownCapture = useCallback(
|
|
95
|
+
(event: KeyboardEvent<HTMLDivElement>) => {
|
|
96
|
+
if (event.key !== "Enter" || event.shiftKey) return;
|
|
97
|
+
// The mention suggestion menu claims Enter with a CAPTURING document
|
|
98
|
+
// listener of its own. If we preventDefault here the highlighted mention
|
|
99
|
+
// is never inserted, so stand down whenever the menu is on screen.
|
|
100
|
+
if (typeof document !== "undefined" && document.querySelector(".bn-suggestion-menu")) return;
|
|
101
|
+
event.preventDefault();
|
|
102
|
+
void submit();
|
|
103
|
+
},
|
|
104
|
+
[submit],
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<div className="flex flex-col gap-1 border-t p-4">
|
|
109
|
+
<div className="bg-muted/30 flex items-end gap-2 rounded-lg border p-2">
|
|
110
|
+
<div className="max-h-40 min-w-0 flex-1 overflow-y-auto" onKeyDownCapture={handleKeyDownCapture}>
|
|
111
|
+
<BlockNoteEditorContainer
|
|
112
|
+
key={resetKey}
|
|
113
|
+
id={editorId}
|
|
114
|
+
type="assistants"
|
|
115
|
+
placeholder={t("features.assistant.composer_placeholder")}
|
|
116
|
+
enableMentions
|
|
117
|
+
mentionSearchFn={mentionSearchFn}
|
|
118
|
+
mentionSearchParams={mentionSearchParams}
|
|
119
|
+
mentionResolveFn={mentionResolveFn}
|
|
120
|
+
suggestionMenuComponent={suggestionMenuComponent}
|
|
121
|
+
onChange={(content: unknown, empty: boolean) => {
|
|
122
|
+
setBlocks(Array.isArray(content) ? content : []);
|
|
123
|
+
setIsEmpty(empty);
|
|
124
|
+
}}
|
|
125
|
+
className="[&_.bn-container]:!p-0"
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
<Button onClick={submit} disabled={!canSend} size="sm" className="h-8">
|
|
129
|
+
<ArrowUp className="mr-1 h-4 w-4" /> {t("features.assistant.send")}
|
|
130
|
+
</Button>
|
|
131
|
+
</div>
|
|
132
|
+
<div className="text-muted-foreground text-right text-xs">{t("features.assistant.keyboard_hint")}</div>
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { useEffect, useRef } from "react";
|
|
4
4
|
import type { AssistantMessageInterface } from "../../../assistant-message/data/AssistantMessageInterface";
|
|
5
5
|
import { MessageList } from "../../../assistant-message/components/MessageList";
|
|
6
|
-
import type { ApprovalActionRenderer } from "../../../assistant-message/components/MessageItem";
|
|
6
|
+
import type { ApprovalActionRenderer, MentionRenderer } from "../../../assistant-message/components/MessageItem";
|
|
7
7
|
import { AssistantStatusLine } from "./AssistantStatusLine";
|
|
8
8
|
|
|
9
9
|
interface Props {
|
|
@@ -14,6 +14,7 @@ interface Props {
|
|
|
14
14
|
failedMessageIds?: Set<string>;
|
|
15
15
|
onRetry?: (tempId: string) => void;
|
|
16
16
|
renderApprovalAction?: ApprovalActionRenderer;
|
|
17
|
+
renderMention?: MentionRenderer;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
export function AssistantThread({
|
|
@@ -24,6 +25,7 @@ export function AssistantThread({
|
|
|
24
25
|
failedMessageIds,
|
|
25
26
|
onRetry,
|
|
26
27
|
renderApprovalAction,
|
|
28
|
+
renderMention,
|
|
27
29
|
}: Props) {
|
|
28
30
|
const endRef = useRef<HTMLDivElement>(null);
|
|
29
31
|
|
|
@@ -39,6 +41,7 @@ export function AssistantThread({
|
|
|
39
41
|
failedMessageIds={failedMessageIds}
|
|
40
42
|
onRetry={onRetry}
|
|
41
43
|
renderApprovalAction={renderApprovalAction}
|
|
44
|
+
renderMention={renderMention}
|
|
42
45
|
/>
|
|
43
46
|
{sending && <AssistantStatusLine status={status} />}
|
|
44
47
|
<div ref={endRef} />
|
|
@@ -20,7 +20,10 @@ interface AssistantContextValue {
|
|
|
20
20
|
sending: boolean;
|
|
21
21
|
status?: string;
|
|
22
22
|
failedMessageIds: Set<string>;
|
|
23
|
-
sendMessage(
|
|
23
|
+
sendMessage(
|
|
24
|
+
content: string,
|
|
25
|
+
opts?: { howToMode?: boolean; limitToHowToId?: string; contentBlocks?: unknown[] },
|
|
26
|
+
): Promise<void>;
|
|
24
27
|
retrySend(tempId: string): Promise<void>;
|
|
25
28
|
selectThread(id: string): Promise<void>;
|
|
26
29
|
startNew(): void;
|
|
@@ -46,6 +49,28 @@ interface Props {
|
|
|
46
49
|
* current route is preserved.
|
|
47
50
|
*/
|
|
48
51
|
manageUrl?: boolean;
|
|
52
|
+
/**
|
|
53
|
+
* Confines the provider to one bound resource (e.g. a campaign): the thread
|
|
54
|
+
* list is filtered to that resource and newly created threads are bound to
|
|
55
|
+
* it, so a scoped surface never shows or creates cross-scope threads.
|
|
56
|
+
*/
|
|
57
|
+
scope?: { type: string; id: string };
|
|
58
|
+
/**
|
|
59
|
+
* Builds the browser URL for a thread. Defaults to `/assistants/{id}` (or
|
|
60
|
+
* `/assistants` when no thread is active), which is only correct for the
|
|
61
|
+
* standalone assistant route; a scoped host passes its own builder.
|
|
62
|
+
*/
|
|
63
|
+
threadUrl?: (id?: string) => string;
|
|
64
|
+
/**
|
|
65
|
+
* Page heading. Defaults to the "Assistants" entity label; a scoped host
|
|
66
|
+
* passes something meaningful for its own surface.
|
|
67
|
+
*/
|
|
68
|
+
titleOverride?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Breadcrumb trail. Defaults to a single crumb pointing at the global
|
|
71
|
+
* assistant list, which is wrong for a scoped surface.
|
|
72
|
+
*/
|
|
73
|
+
breadcrumbsOverride?: BreadcrumbItemData[];
|
|
49
74
|
}
|
|
50
75
|
|
|
51
76
|
function stripOptimistic(list: AssistantMessageInterface[]): AssistantMessageInterface[] {
|
|
@@ -67,10 +92,24 @@ function withPatchedTitle(source: AssistantInterface, title: string): AssistantI
|
|
|
67
92
|
});
|
|
68
93
|
}
|
|
69
94
|
|
|
70
|
-
export function AssistantProvider({
|
|
95
|
+
export function AssistantProvider({
|
|
96
|
+
children,
|
|
97
|
+
dehydratedAssistant,
|
|
98
|
+
dehydratedMessages,
|
|
99
|
+
manageUrl = true,
|
|
100
|
+
titleOverride,
|
|
101
|
+
breadcrumbsOverride,
|
|
102
|
+
scope,
|
|
103
|
+
threadUrl,
|
|
104
|
+
}: Props) {
|
|
71
105
|
const t = useTranslations();
|
|
72
106
|
const generateUrl = usePageUrlGenerator();
|
|
73
107
|
|
|
108
|
+
const resolveThreadUrl = useCallback(
|
|
109
|
+
(id?: string) => (threadUrl ? threadUrl(id) : id ? `/assistants/${id}` : "/assistants"),
|
|
110
|
+
[threadUrl],
|
|
111
|
+
);
|
|
112
|
+
|
|
74
113
|
const [assistant, setAssistant] = useState<AssistantInterface | undefined>(() =>
|
|
75
114
|
dehydratedAssistant ? rehydrate<AssistantInterface>(Modules.Assistant, dehydratedAssistant) : undefined,
|
|
76
115
|
);
|
|
@@ -91,7 +130,7 @@ export function AssistantProvider({ children, dehydratedAssistant, dehydratedMes
|
|
|
91
130
|
const { socket } = useSocketContext();
|
|
92
131
|
|
|
93
132
|
const sendMessage = useCallback(
|
|
94
|
-
async (content: string, opts?: { howToMode?: boolean; limitToHowToId?: string }) => {
|
|
133
|
+
async (content: string, opts?: { howToMode?: boolean; limitToHowToId?: string; contentBlocks?: unknown[] }) => {
|
|
95
134
|
const trimmed = content.trim();
|
|
96
135
|
if (!trimmed) return;
|
|
97
136
|
|
|
@@ -116,6 +155,8 @@ export function AssistantProvider({ children, dehydratedAssistant, dehydratedMes
|
|
|
116
155
|
firstMessage: trimmed,
|
|
117
156
|
howToMode: opts?.howToMode,
|
|
118
157
|
limitToHowToId: opts?.limitToHowToId,
|
|
158
|
+
contentBlocks: opts?.contentBlocks,
|
|
159
|
+
boundContent: scope,
|
|
119
160
|
};
|
|
120
161
|
const created = operatorMode
|
|
121
162
|
? await AssistantService.createOperator(input)
|
|
@@ -125,19 +166,21 @@ export function AssistantProvider({ children, dehydratedAssistant, dehydratedMes
|
|
|
125
166
|
setMessages(msgs);
|
|
126
167
|
setThreads((prev) => [created, ...prev]);
|
|
127
168
|
if (manageUrl && typeof window !== "undefined") {
|
|
128
|
-
window.history.replaceState(null, "",
|
|
169
|
+
window.history.replaceState(null, "", resolveThreadUrl(created.id));
|
|
129
170
|
}
|
|
130
171
|
} else {
|
|
131
172
|
const result = operatorMode
|
|
132
173
|
? await AssistantService.appendMessageOperator({
|
|
133
174
|
assistantId: assistant.id,
|
|
134
175
|
content: trimmed,
|
|
176
|
+
contentBlocks: opts?.contentBlocks,
|
|
135
177
|
})
|
|
136
178
|
: await AssistantService.appendMessage({
|
|
137
179
|
assistantId: assistant.id,
|
|
138
180
|
content: trimmed,
|
|
139
181
|
howToMode: opts?.howToMode,
|
|
140
182
|
limitToHowToId: opts?.limitToHowToId,
|
|
183
|
+
contentBlocks: opts?.contentBlocks,
|
|
141
184
|
});
|
|
142
185
|
setMessages((prev) => [...stripOptimistic(prev), ...result]);
|
|
143
186
|
}
|
|
@@ -153,7 +196,7 @@ export function AssistantProvider({ children, dehydratedAssistant, dehydratedMes
|
|
|
153
196
|
setStatus(undefined);
|
|
154
197
|
}
|
|
155
198
|
},
|
|
156
|
-
[assistant, messages, socket, operatorMode],
|
|
199
|
+
[assistant, messages, socket, operatorMode, scope, manageUrl, resolveThreadUrl],
|
|
157
200
|
);
|
|
158
201
|
|
|
159
202
|
const appendResolvedMessage = useCallback((message: AssistantMessageInterface) => {
|
|
@@ -191,10 +234,10 @@ export function AssistantProvider({ children, dehydratedAssistant, dehydratedMes
|
|
|
191
234
|
// absent) keeps the default responder flow.
|
|
192
235
|
setOperatorMode(target.engine === "operator");
|
|
193
236
|
if (manageUrl && typeof window !== "undefined") {
|
|
194
|
-
window.history.replaceState(null, "",
|
|
237
|
+
window.history.replaceState(null, "", resolveThreadUrl(id));
|
|
195
238
|
}
|
|
196
239
|
},
|
|
197
|
-
[manageUrl],
|
|
240
|
+
[manageUrl, resolveThreadUrl],
|
|
198
241
|
);
|
|
199
242
|
|
|
200
243
|
const renameThread = useCallback(async (id: string, title: string) => {
|
|
@@ -209,9 +252,9 @@ export function AssistantProvider({ children, dehydratedAssistant, dehydratedMes
|
|
|
209
252
|
setFailedMessageIds(new Set());
|
|
210
253
|
setOperatorMode(false);
|
|
211
254
|
if (manageUrl && typeof window !== "undefined") {
|
|
212
|
-
window.history.replaceState(null, "",
|
|
255
|
+
window.history.replaceState(null, "", resolveThreadUrl());
|
|
213
256
|
}
|
|
214
|
-
}, [manageUrl]);
|
|
257
|
+
}, [manageUrl, resolveThreadUrl]);
|
|
215
258
|
|
|
216
259
|
const deleteThread = useCallback(async (id: string) => {
|
|
217
260
|
await AssistantService.delete({ id });
|
|
@@ -230,7 +273,7 @@ export function AssistantProvider({ children, dehydratedAssistant, dehydratedMes
|
|
|
230
273
|
(async () => {
|
|
231
274
|
setThreadsLoading(true);
|
|
232
275
|
try {
|
|
233
|
-
const loaded = await AssistantService.findMany();
|
|
276
|
+
const loaded = await AssistantService.findMany({ boundType: scope?.type, boundId: scope?.id });
|
|
234
277
|
if (!cancelled) setThreads(loaded);
|
|
235
278
|
} finally {
|
|
236
279
|
if (!cancelled) setThreadsLoading(false);
|
|
@@ -239,7 +282,7 @@ export function AssistantProvider({ children, dehydratedAssistant, dehydratedMes
|
|
|
239
282
|
return () => {
|
|
240
283
|
cancelled = true;
|
|
241
284
|
};
|
|
242
|
-
}, []);
|
|
285
|
+
}, [scope?.type, scope?.id]);
|
|
243
286
|
|
|
244
287
|
const value = useMemo<AssistantContextValue>(
|
|
245
288
|
() => ({
|
|
@@ -279,15 +322,19 @@ export function AssistantProvider({ children, dehydratedAssistant, dehydratedMes
|
|
|
279
322
|
],
|
|
280
323
|
);
|
|
281
324
|
|
|
282
|
-
|
|
325
|
+
// A scoped assistant lives under its host resource, so the default trail to
|
|
326
|
+
// the global /assistants list is wrong there — the consumer supplies its own.
|
|
327
|
+
const breadcrumbs: BreadcrumbItemData[] = breadcrumbsOverride ?? [
|
|
283
328
|
{
|
|
284
329
|
name: t("entities.assistants", { count: 2 }),
|
|
285
330
|
href: generateUrl({ page: Modules.Assistant }),
|
|
286
331
|
},
|
|
287
332
|
];
|
|
288
333
|
|
|
334
|
+
// `entities.tasks` was a copy-paste from another feature and rendered
|
|
335
|
+
// "Tasks" as the page heading of every assistant page.
|
|
289
336
|
const title = {
|
|
290
|
-
type: t("entities.
|
|
337
|
+
type: titleOverride ?? t("entities.assistants", { count: 2 }),
|
|
291
338
|
};
|
|
292
339
|
|
|
293
340
|
return (
|