@assistant-ui/react 0.12.19 → 0.12.21
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/README.md +1 -1
- package/dist/client/ExternalThread.d.ts +24 -3
- package/dist/client/ExternalThread.d.ts.map +1 -1
- package/dist/client/ExternalThread.js +106 -27
- package/dist/client/ExternalThread.js.map +1 -1
- package/dist/client/InMemoryThreadList.js +23 -30
- package/dist/client/InMemoryThreadList.js.map +1 -1
- package/dist/client/SingleThreadList.d.ts +12 -0
- package/dist/client/SingleThreadList.d.ts.map +1 -0
- package/dist/client/SingleThreadList.js +68 -0
- package/dist/client/SingleThreadList.js.map +1 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/primitives/composer/ComposerInput.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerInput.js +37 -7
- package/dist/primitives/composer/ComposerInput.js.map +1 -1
- package/dist/primitives/composer/ComposerQueue.d.ts +2 -0
- package/dist/primitives/composer/ComposerQueue.d.ts.map +1 -0
- package/dist/primitives/composer/ComposerQueue.js +3 -0
- package/dist/primitives/composer/ComposerQueue.js.map +1 -0
- package/dist/primitives/composer/ComposerSend.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerSend.js +3 -1
- package/dist/primitives/composer/ComposerSend.js.map +1 -1
- package/dist/primitives/composer/mention/ComposerMentionBack.d.ts +21 -0
- package/dist/primitives/composer/mention/ComposerMentionBack.d.ts.map +1 -0
- package/dist/primitives/composer/mention/ComposerMentionBack.js +28 -0
- package/dist/primitives/composer/mention/ComposerMentionBack.js.map +1 -0
- package/dist/primitives/composer/mention/ComposerMentionCategories.d.ts +42 -0
- package/dist/primitives/composer/mention/ComposerMentionCategories.d.ts.map +1 -0
- package/dist/primitives/composer/mention/ComposerMentionCategories.js +32 -0
- package/dist/primitives/composer/mention/ComposerMentionCategories.js.map +1 -0
- package/dist/primitives/composer/mention/ComposerMentionContext.d.ts +23 -0
- package/dist/primitives/composer/mention/ComposerMentionContext.d.ts.map +1 -0
- package/dist/primitives/composer/mention/ComposerMentionContext.js +66 -0
- package/dist/primitives/composer/mention/ComposerMentionContext.js.map +1 -0
- package/dist/primitives/composer/mention/ComposerMentionItems.d.ts +46 -0
- package/dist/primitives/composer/mention/ComposerMentionItems.d.ts.map +1 -0
- package/dist/primitives/composer/mention/ComposerMentionItems.js +30 -0
- package/dist/primitives/composer/mention/ComposerMentionItems.js.map +1 -0
- package/dist/primitives/composer/mention/ComposerMentionPopover.d.ts +24 -0
- package/dist/primitives/composer/mention/ComposerMentionPopover.d.ts.map +1 -0
- package/dist/primitives/composer/mention/ComposerMentionPopover.js +28 -0
- package/dist/primitives/composer/mention/ComposerMentionPopover.js.map +1 -0
- package/dist/primitives/composer/mention/MentionResource.d.ts +39 -0
- package/dist/primitives/composer/mention/MentionResource.d.ts.map +1 -0
- package/dist/primitives/composer/mention/MentionResource.js +230 -0
- package/dist/primitives/composer/mention/MentionResource.js.map +1 -0
- package/dist/primitives/composer/mention/detectMentionTrigger.d.ts +2 -0
- package/dist/primitives/composer/mention/detectMentionTrigger.d.ts.map +1 -0
- package/dist/primitives/composer/mention/detectMentionTrigger.js +26 -0
- package/dist/primitives/composer/mention/detectMentionTrigger.js.map +1 -0
- package/dist/primitives/composer/mention/index.d.ts +6 -0
- package/dist/primitives/composer/mention/index.d.ts.map +1 -0
- package/dist/primitives/composer/mention/index.js +6 -0
- package/dist/primitives/composer/mention/index.js.map +1 -0
- package/dist/primitives/composer.d.ts +10 -0
- package/dist/primitives/composer.d.ts.map +1 -1
- package/dist/primitives/composer.js +10 -0
- package/dist/primitives/composer.js.map +1 -1
- package/dist/primitives/queueItem/QueueItemRemove.d.ts +19 -0
- package/dist/primitives/queueItem/QueueItemRemove.d.ts.map +1 -0
- package/dist/primitives/queueItem/QueueItemRemove.js +21 -0
- package/dist/primitives/queueItem/QueueItemRemove.js.map +1 -0
- package/dist/primitives/queueItem/QueueItemSteer.d.ts +19 -0
- package/dist/primitives/queueItem/QueueItemSteer.d.ts.map +1 -0
- package/dist/primitives/queueItem/QueueItemSteer.js +21 -0
- package/dist/primitives/queueItem/QueueItemSteer.js.map +1 -0
- package/dist/primitives/queueItem/QueueItemText.d.ts +18 -0
- package/dist/primitives/queueItem/QueueItemText.d.ts.map +1 -0
- package/dist/primitives/queueItem/QueueItemText.js +19 -0
- package/dist/primitives/queueItem/QueueItemText.js.map +1 -0
- package/dist/primitives/queueItem.d.ts +4 -0
- package/dist/primitives/queueItem.d.ts.map +1 -0
- package/dist/primitives/queueItem.js +4 -0
- package/dist/primitives/queueItem.js.map +1 -0
- package/dist/unstable/useToolMentionAdapter.d.ts +42 -0
- package/dist/unstable/useToolMentionAdapter.d.ts.map +1 -0
- package/dist/unstable/useToolMentionAdapter.js +65 -0
- package/dist/unstable/useToolMentionAdapter.js.map +1 -0
- package/package.json +10 -10
- package/src/client/ExternalThread.ts +160 -32
- package/src/client/InMemoryThreadList.ts +24 -35
- package/src/client/SingleThreadList.ts +95 -0
- package/src/index.ts +26 -0
- package/src/primitives/composer/ComposerInput.tsx +49 -5
- package/src/primitives/composer/ComposerQueue.tsx +3 -0
- package/src/primitives/composer/ComposerSend.ts +3 -1
- package/src/primitives/composer/mention/ComposerMentionBack.tsx +55 -0
- package/src/primitives/composer/mention/ComposerMentionCategories.tsx +104 -0
- package/src/primitives/composer/mention/ComposerMentionContext.tsx +141 -0
- package/src/primitives/composer/mention/ComposerMentionItems.tsx +104 -0
- package/src/primitives/composer/mention/ComposerMentionPopover.tsx +52 -0
- package/src/primitives/composer/mention/MentionResource.ts +328 -0
- package/src/primitives/composer/mention/detectMentionTrigger.test.ts +78 -0
- package/src/primitives/composer/mention/detectMentionTrigger.ts +37 -0
- package/src/primitives/composer/mention/index.ts +16 -0
- package/src/primitives/composer.ts +10 -0
- package/src/primitives/queueItem/QueueItemRemove.ts +37 -0
- package/src/primitives/queueItem/QueueItemSteer.ts +37 -0
- package/src/primitives/queueItem/QueueItemText.tsx +37 -0
- package/src/primitives/queueItem.ts +3 -0
- package/src/tests/BaseComposerRuntimeCore.test.ts +3 -1
- package/src/unstable/useToolMentionAdapter.ts +114 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { resource, tapMemo } from "@assistant-ui/tap";
|
|
2
|
+
import {
|
|
3
|
+
type ClientElement,
|
|
4
|
+
type ClientOutput,
|
|
5
|
+
tapClientResource,
|
|
6
|
+
} from "@assistant-ui/store";
|
|
7
|
+
|
|
8
|
+
const THREAD_ID = "default";
|
|
9
|
+
|
|
10
|
+
const SingleThreadListItem = resource((): ClientOutput<"threadListItem"> => {
|
|
11
|
+
return {
|
|
12
|
+
getState: () => ({
|
|
13
|
+
id: THREAD_ID,
|
|
14
|
+
remoteId: undefined,
|
|
15
|
+
externalId: undefined,
|
|
16
|
+
title: undefined,
|
|
17
|
+
status: "regular",
|
|
18
|
+
}),
|
|
19
|
+
switchTo: () => {},
|
|
20
|
+
rename: () => {},
|
|
21
|
+
archive: () => {},
|
|
22
|
+
unarchive: () => {},
|
|
23
|
+
delete: () => {},
|
|
24
|
+
generateTitle: () => {},
|
|
25
|
+
initialize: async () => ({ remoteId: THREAD_ID, externalId: undefined }),
|
|
26
|
+
detach: () => {},
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
type SingleThreadListProps = {
|
|
31
|
+
thread: ClientElement<"thread">;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* A minimal threads scope that wraps a single thread.
|
|
36
|
+
* Automatically provided by ExternalThread when no threads scope exists.
|
|
37
|
+
* Mounts the provided thread resource element.
|
|
38
|
+
*/
|
|
39
|
+
export const SingleThreadList = resource(
|
|
40
|
+
({ thread }: SingleThreadListProps): ClientOutput<"threads"> => {
|
|
41
|
+
const itemClient = tapClientResource(SingleThreadListItem());
|
|
42
|
+
const threadClient = tapClientResource(thread);
|
|
43
|
+
|
|
44
|
+
const state = tapMemo(
|
|
45
|
+
() => ({
|
|
46
|
+
mainThreadId: THREAD_ID,
|
|
47
|
+
newThreadId: null,
|
|
48
|
+
isLoading: false,
|
|
49
|
+
threadIds: [THREAD_ID],
|
|
50
|
+
archivedThreadIds: [],
|
|
51
|
+
threadItems: [itemClient.state],
|
|
52
|
+
main: threadClient.state,
|
|
53
|
+
}),
|
|
54
|
+
[itemClient.state, threadClient.state],
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
getState: () => state,
|
|
59
|
+
switchToThread: () => {
|
|
60
|
+
throw new Error("SingleThreadList does not support switchToThread");
|
|
61
|
+
},
|
|
62
|
+
switchToNewThread: () => {
|
|
63
|
+
throw new Error("SingleThreadList does not support switchToNewThread");
|
|
64
|
+
},
|
|
65
|
+
item: (selector) => {
|
|
66
|
+
if (
|
|
67
|
+
selector !== "main" &&
|
|
68
|
+
!(
|
|
69
|
+
typeof selector === "object" &&
|
|
70
|
+
"id" in selector &&
|
|
71
|
+
selector.id === THREAD_ID
|
|
72
|
+
) &&
|
|
73
|
+
!(
|
|
74
|
+
typeof selector === "object" &&
|
|
75
|
+
"index" in selector &&
|
|
76
|
+
selector.index === 0
|
|
77
|
+
)
|
|
78
|
+
) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`SingleThreadList: unknown item selector ${JSON.stringify(selector)}`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
return itemClient.methods;
|
|
84
|
+
},
|
|
85
|
+
thread: (selector) => {
|
|
86
|
+
if (selector !== "main" && selector !== THREAD_ID) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
`SingleThreadList: unknown thread selector ${JSON.stringify(selector)}`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
return threadClient.methods;
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
);
|
package/src/index.ts
CHANGED
|
@@ -199,6 +199,13 @@ export {
|
|
|
199
199
|
type ToolDefinition,
|
|
200
200
|
Tools,
|
|
201
201
|
DataRenderers,
|
|
202
|
+
Interactables,
|
|
203
|
+
useInteractable,
|
|
204
|
+
type UseInteractableConfig,
|
|
205
|
+
type UseInteractableMetadata,
|
|
206
|
+
makeInteractable,
|
|
207
|
+
type InteractableConfig,
|
|
208
|
+
type AssistantInteractable,
|
|
202
209
|
} from "@assistant-ui/core/react";
|
|
203
210
|
|
|
204
211
|
// Core pass-through (unchanged)
|
|
@@ -216,6 +223,11 @@ export type { Tool } from "assistant-stream";
|
|
|
216
223
|
export { tool } from "@assistant-ui/core";
|
|
217
224
|
|
|
218
225
|
export { Suggestions, type SuggestionConfig } from "@assistant-ui/core/store";
|
|
226
|
+
export type {
|
|
227
|
+
QueueItemState,
|
|
228
|
+
QueueItemMethods,
|
|
229
|
+
} from "@assistant-ui/core/store";
|
|
230
|
+
export type { ComposerSendOptions } from "@assistant-ui/core/store";
|
|
219
231
|
|
|
220
232
|
// React-only (stays)
|
|
221
233
|
export { makeAssistantVisible } from "./model-context/makeAssistantVisible";
|
|
@@ -250,6 +262,7 @@ export * as AttachmentPrimitive from "./primitives/attachment";
|
|
|
250
262
|
export * as BranchPickerPrimitive from "./primitives/branchPicker";
|
|
251
263
|
export * as ChainOfThoughtPrimitive from "./primitives/chainOfThought";
|
|
252
264
|
export * as ComposerPrimitive from "./primitives/composer";
|
|
265
|
+
export * as QueueItemPrimitive from "./primitives/queueItem";
|
|
253
266
|
export * as MessagePartPrimitive from "./primitives/messagePart";
|
|
254
267
|
export * as ErrorPrimitive from "./primitives/error";
|
|
255
268
|
export * as MessagePrimitive from "./primitives/message";
|
|
@@ -348,16 +361,29 @@ export {
|
|
|
348
361
|
ExternalThread,
|
|
349
362
|
type ExternalThreadProps,
|
|
350
363
|
type ExternalThreadMessage,
|
|
364
|
+
type ExternalThreadQueueAdapter,
|
|
351
365
|
} from "./client/ExternalThread";
|
|
352
366
|
export {
|
|
353
367
|
InMemoryThreadList,
|
|
354
368
|
type InMemoryThreadListProps,
|
|
355
369
|
} from "./client/InMemoryThreadList";
|
|
370
|
+
export { SingleThreadList } from "./client/SingleThreadList";
|
|
356
371
|
|
|
357
372
|
// ============================================================================
|
|
358
373
|
// internal & augmentations
|
|
359
374
|
// ============================================================================
|
|
360
375
|
export * as INTERNAL from "./internal";
|
|
376
|
+
|
|
377
|
+
// Unstable - mention
|
|
378
|
+
export {
|
|
379
|
+
unstable_useToolMentionAdapter,
|
|
380
|
+
type Unstable_ToolMentionAdapterOptions,
|
|
381
|
+
} from "./unstable/useToolMentionAdapter";
|
|
382
|
+
export {
|
|
383
|
+
useMentionContext as unstable_useMentionContext,
|
|
384
|
+
useMentionContextOptional as unstable_useMentionContextOptional,
|
|
385
|
+
useMentionInternalContext as unstable_useMentionInternalContext,
|
|
386
|
+
} from "./primitives/composer/mention";
|
|
361
387
|
export type { ToolExecutionStatus } from "./internal";
|
|
362
388
|
|
|
363
389
|
export type { Assistant } from "./augmentations";
|
|
@@ -18,6 +18,10 @@ import { useEscapeKeydown } from "@radix-ui/react-use-escape-keydown";
|
|
|
18
18
|
import { useOnScrollToBottom } from "../../utils/hooks/useOnScrollToBottom";
|
|
19
19
|
import { useAuiState, useAui } from "@assistant-ui/store";
|
|
20
20
|
import { flushResourcesSync } from "@assistant-ui/tap";
|
|
21
|
+
import {
|
|
22
|
+
useMentionContextOptional,
|
|
23
|
+
useMentionInternalContext,
|
|
24
|
+
} from "./mention/ComposerMentionContext";
|
|
21
25
|
|
|
22
26
|
export namespace ComposerPrimitiveInput {
|
|
23
27
|
export type Element = HTMLTextAreaElement;
|
|
@@ -118,6 +122,7 @@ export const ComposerPrimitiveInput = forwardRef<
|
|
|
118
122
|
onChange,
|
|
119
123
|
onKeyDown,
|
|
120
124
|
onPaste,
|
|
125
|
+
onSelect,
|
|
121
126
|
submitOnEnter,
|
|
122
127
|
submitMode,
|
|
123
128
|
cancelOnEscape = true,
|
|
@@ -130,6 +135,8 @@ export const ComposerPrimitiveInput = forwardRef<
|
|
|
130
135
|
forwardedRef,
|
|
131
136
|
) => {
|
|
132
137
|
const aui = useAui();
|
|
138
|
+
const mentionContext = useMentionContextOptional();
|
|
139
|
+
const mentionInternalContext = useMentionInternalContext();
|
|
133
140
|
|
|
134
141
|
const effectiveSubmitMode =
|
|
135
142
|
submitMode ?? (submitOnEnter === false ? "none" : "enter");
|
|
@@ -149,11 +156,17 @@ export const ComposerPrimitiveInput = forwardRef<
|
|
|
149
156
|
const ref = useComposedRefs(forwardedRef, textareaRef);
|
|
150
157
|
|
|
151
158
|
useEscapeKeydown((e) => {
|
|
152
|
-
if (!cancelOnEscape) return;
|
|
153
|
-
|
|
154
159
|
// Only handle ESC if it originated from within this input
|
|
155
160
|
if (!textareaRef.current?.contains(e.target as Node)) return;
|
|
156
161
|
|
|
162
|
+
// Let mention popover handle Escape first
|
|
163
|
+
if (mentionContext?.open) {
|
|
164
|
+
mentionContext.handleKeyDown(e);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (!cancelOnEscape) return;
|
|
169
|
+
|
|
157
170
|
const composer = aui.composer();
|
|
158
171
|
if (composer.getState().canCancel) {
|
|
159
172
|
composer.cancel();
|
|
@@ -167,9 +180,31 @@ export const ComposerPrimitiveInput = forwardRef<
|
|
|
167
180
|
// ignore IME composition events
|
|
168
181
|
if (e.nativeEvent.isComposing) return;
|
|
169
182
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
183
|
+
// Let the mention popover handle keyboard events first
|
|
184
|
+
if (mentionContext?.handleKeyDown(e)) return;
|
|
185
|
+
|
|
186
|
+
if (e.key === "Enter") {
|
|
187
|
+
const threadState = aui.thread().getState();
|
|
188
|
+
const hasQueue = threadState.capabilities.queue;
|
|
189
|
+
|
|
190
|
+
// Steer hotkey: Cmd/Ctrl+Shift+Enter (respects submitMode="none" and isEmpty)
|
|
191
|
+
if (
|
|
192
|
+
e.shiftKey &&
|
|
193
|
+
(e.ctrlKey || e.metaKey) &&
|
|
194
|
+
hasQueue &&
|
|
195
|
+
effectiveSubmitMode !== "none" &&
|
|
196
|
+
!aui.composer().getState().isEmpty
|
|
197
|
+
) {
|
|
198
|
+
e.preventDefault();
|
|
199
|
+
aui.composer().send({ steer: true });
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Regular newline: Shift+Enter
|
|
204
|
+
if (e.shiftKey) return;
|
|
205
|
+
|
|
206
|
+
// Block submission when running unless queue is supported
|
|
207
|
+
if (threadState.isRunning && !hasQueue) return;
|
|
173
208
|
|
|
174
209
|
let shouldSubmit = false;
|
|
175
210
|
if (effectiveSubmitMode === "ctrlEnter") {
|
|
@@ -254,8 +289,17 @@ export const ComposerPrimitiveInput = forwardRef<
|
|
|
254
289
|
flushResourcesSync(() => {
|
|
255
290
|
aui.composer().setText(e.target.value);
|
|
256
291
|
});
|
|
292
|
+
mentionInternalContext?.setCursorPosition(
|
|
293
|
+
e.target.selectionStart ?? e.target.value.length,
|
|
294
|
+
);
|
|
257
295
|
})}
|
|
258
296
|
onKeyDown={composeEventHandlers(onKeyDown, handleKeyPress)}
|
|
297
|
+
onSelect={composeEventHandlers(onSelect, (e) => {
|
|
298
|
+
const target = e.target as HTMLTextAreaElement;
|
|
299
|
+
mentionInternalContext?.setCursorPosition(
|
|
300
|
+
target.selectionStart ?? target.value.length,
|
|
301
|
+
);
|
|
302
|
+
})}
|
|
259
303
|
onPaste={composeEventHandlers(onPaste, handlePaste)}
|
|
260
304
|
/>
|
|
261
305
|
);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useCallback } from "react";
|
|
3
4
|
import {
|
|
4
5
|
ActionButtonElement,
|
|
5
6
|
ActionButtonProps,
|
|
@@ -9,8 +10,9 @@ import { useComposerSend as useComposerSendBehavior } from "@assistant-ui/core/r
|
|
|
9
10
|
|
|
10
11
|
export const useComposerSend = () => {
|
|
11
12
|
const { disabled, send } = useComposerSendBehavior();
|
|
13
|
+
const callback = useCallback(() => send(), [send]);
|
|
12
14
|
if (disabled) return null;
|
|
13
|
-
return
|
|
15
|
+
return callback;
|
|
14
16
|
};
|
|
15
17
|
|
|
16
18
|
export namespace ComposerPrimitiveSend {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Primitive } from "@radix-ui/react-primitive";
|
|
4
|
+
import {
|
|
5
|
+
type ComponentRef,
|
|
6
|
+
type ComponentPropsWithoutRef,
|
|
7
|
+
forwardRef,
|
|
8
|
+
useCallback,
|
|
9
|
+
} from "react";
|
|
10
|
+
import { composeEventHandlers } from "@radix-ui/primitive";
|
|
11
|
+
import { useMentionContext } from "./ComposerMentionContext";
|
|
12
|
+
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// MentionBack — Button to navigate back from items to categories
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
export namespace ComposerPrimitiveMentionBack {
|
|
18
|
+
export type Element = ComponentRef<typeof Primitive.button>;
|
|
19
|
+
export type Props = ComponentPropsWithoutRef<typeof Primitive.button>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A button that navigates back from category items to the category list.
|
|
24
|
+
* Only renders when a category is active (drill-down view).
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```tsx
|
|
28
|
+
* <ComposerPrimitive.MentionBack>
|
|
29
|
+
* ← Back
|
|
30
|
+
* </ComposerPrimitive.MentionBack>
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export const ComposerPrimitiveMentionBack = forwardRef<
|
|
34
|
+
ComposerPrimitiveMentionBack.Element,
|
|
35
|
+
ComposerPrimitiveMentionBack.Props
|
|
36
|
+
>(({ onClick, ...props }, forwardedRef) => {
|
|
37
|
+
const { activeCategoryId, isSearchMode, goBack } = useMentionContext();
|
|
38
|
+
|
|
39
|
+
const handleClick = useCallback(() => {
|
|
40
|
+
goBack();
|
|
41
|
+
}, [goBack]);
|
|
42
|
+
|
|
43
|
+
if (!activeCategoryId || isSearchMode) return null;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Primitive.button
|
|
47
|
+
type="button"
|
|
48
|
+
{...props}
|
|
49
|
+
ref={forwardedRef}
|
|
50
|
+
onClick={composeEventHandlers(onClick, handleClick)}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
ComposerPrimitiveMentionBack.displayName = "ComposerPrimitive.MentionBack";
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Primitive } from "@radix-ui/react-primitive";
|
|
4
|
+
import {
|
|
5
|
+
type ComponentRef,
|
|
6
|
+
type ComponentPropsWithoutRef,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
forwardRef,
|
|
9
|
+
useCallback,
|
|
10
|
+
} from "react";
|
|
11
|
+
import { composeEventHandlers } from "@radix-ui/primitive";
|
|
12
|
+
import { useMentionContext } from "./ComposerMentionContext";
|
|
13
|
+
import type { Unstable_MentionCategory } from "@assistant-ui/core";
|
|
14
|
+
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// MentionCategories — Renders the list of categories
|
|
17
|
+
// =============================================================================
|
|
18
|
+
|
|
19
|
+
export namespace ComposerPrimitiveMentionCategories {
|
|
20
|
+
export type Element = ComponentRef<typeof Primitive.div>;
|
|
21
|
+
export type Props = Omit<
|
|
22
|
+
ComponentPropsWithoutRef<typeof Primitive.div>,
|
|
23
|
+
"children"
|
|
24
|
+
> & {
|
|
25
|
+
/**
|
|
26
|
+
* Render function that receives the filtered categories and returns
|
|
27
|
+
* the UI. A render-function pattern is used here (instead of a
|
|
28
|
+
* `components` prop) to give consumers full control over list layout,
|
|
29
|
+
* ordering, grouping, and empty states.
|
|
30
|
+
*/
|
|
31
|
+
children: (categories: readonly Unstable_MentionCategory[]) => ReactNode;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const ComposerPrimitiveMentionCategories = forwardRef<
|
|
36
|
+
ComposerPrimitiveMentionCategories.Element,
|
|
37
|
+
ComposerPrimitiveMentionCategories.Props
|
|
38
|
+
>(({ children, ...props }, forwardedRef) => {
|
|
39
|
+
const { categories, activeCategoryId, isSearchMode } = useMentionContext();
|
|
40
|
+
|
|
41
|
+
if (activeCategoryId || isSearchMode) return null;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Primitive.div role="group" {...props} ref={forwardedRef}>
|
|
45
|
+
{children(categories)}
|
|
46
|
+
</Primitive.div>
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
ComposerPrimitiveMentionCategories.displayName =
|
|
51
|
+
"ComposerPrimitive.MentionCategories";
|
|
52
|
+
|
|
53
|
+
// =============================================================================
|
|
54
|
+
// MentionCategoryItem — A single category row (clickable to drill-down)
|
|
55
|
+
// =============================================================================
|
|
56
|
+
|
|
57
|
+
export namespace ComposerPrimitiveMentionCategoryItem {
|
|
58
|
+
export type Element = ComponentRef<typeof Primitive.button>;
|
|
59
|
+
export type Props = ComponentPropsWithoutRef<typeof Primitive.button> & {
|
|
60
|
+
categoryId: string;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A button that selects a category and triggers drill-down navigation.
|
|
66
|
+
* Automatically receives `data-highlighted` when keyboard-navigated.
|
|
67
|
+
*/
|
|
68
|
+
export const ComposerPrimitiveMentionCategoryItem = forwardRef<
|
|
69
|
+
ComposerPrimitiveMentionCategoryItem.Element,
|
|
70
|
+
ComposerPrimitiveMentionCategoryItem.Props
|
|
71
|
+
>(({ categoryId, onClick, ...props }, forwardedRef) => {
|
|
72
|
+
const {
|
|
73
|
+
selectCategory,
|
|
74
|
+
categories,
|
|
75
|
+
highlightedIndex,
|
|
76
|
+
activeCategoryId,
|
|
77
|
+
isSearchMode,
|
|
78
|
+
} = useMentionContext();
|
|
79
|
+
|
|
80
|
+
const handleClick = useCallback(() => {
|
|
81
|
+
selectCategory(categoryId);
|
|
82
|
+
}, [selectCategory, categoryId]);
|
|
83
|
+
|
|
84
|
+
// Derive highlighted state from context — no manual wiring needed
|
|
85
|
+
const isHighlighted =
|
|
86
|
+
!activeCategoryId &&
|
|
87
|
+
!isSearchMode &&
|
|
88
|
+
categories.findIndex((c) => c.id === categoryId) === highlightedIndex;
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<Primitive.button
|
|
92
|
+
type="button"
|
|
93
|
+
role="option"
|
|
94
|
+
aria-selected={isHighlighted}
|
|
95
|
+
data-highlighted={isHighlighted ? "" : undefined}
|
|
96
|
+
{...props}
|
|
97
|
+
ref={forwardedRef}
|
|
98
|
+
onClick={composeEventHandlers(onClick, handleClick)}
|
|
99
|
+
/>
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
ComposerPrimitiveMentionCategoryItem.displayName =
|
|
104
|
+
"ComposerPrimitive.MentionCategoryItem";
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createContext,
|
|
5
|
+
useContext,
|
|
6
|
+
useState,
|
|
7
|
+
useCallback,
|
|
8
|
+
useMemo,
|
|
9
|
+
useEffect,
|
|
10
|
+
type ReactNode,
|
|
11
|
+
type FC,
|
|
12
|
+
} from "react";
|
|
13
|
+
import { useResource } from "@assistant-ui/tap/react";
|
|
14
|
+
import { useAui, useAuiState } from "@assistant-ui/store";
|
|
15
|
+
import type {
|
|
16
|
+
Unstable_MentionAdapter,
|
|
17
|
+
Unstable_DirectiveFormatter,
|
|
18
|
+
} from "@assistant-ui/core";
|
|
19
|
+
import { unstable_defaultDirectiveFormatter } from "@assistant-ui/core";
|
|
20
|
+
import {
|
|
21
|
+
MentionResource,
|
|
22
|
+
type MentionResourceOutput,
|
|
23
|
+
type SelectItemOverride,
|
|
24
|
+
} from "./MentionResource";
|
|
25
|
+
|
|
26
|
+
// =============================================================================
|
|
27
|
+
// Context — public (popover components read state + actions from here)
|
|
28
|
+
// =============================================================================
|
|
29
|
+
|
|
30
|
+
const MentionContext = createContext<MentionResourceOutput | null>(null);
|
|
31
|
+
|
|
32
|
+
export const useMentionContext = () => {
|
|
33
|
+
const ctx = useContext(MentionContext);
|
|
34
|
+
if (!ctx)
|
|
35
|
+
throw new Error(
|
|
36
|
+
"useMentionContext must be used within ComposerPrimitive.MentionRoot",
|
|
37
|
+
);
|
|
38
|
+
return ctx;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const useMentionContextOptional = () => {
|
|
42
|
+
return useContext(MentionContext);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// =============================================================================
|
|
46
|
+
// Internal context — ComposerInput → MentionRoot communication
|
|
47
|
+
// =============================================================================
|
|
48
|
+
|
|
49
|
+
type MentionInternalContextValue = {
|
|
50
|
+
setCursorPosition(pos: number): void;
|
|
51
|
+
registerSelectItemOverride(fn: SelectItemOverride): () => void;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const MentionInternalContext =
|
|
55
|
+
createContext<MentionInternalContextValue | null>(null);
|
|
56
|
+
|
|
57
|
+
export const useMentionInternalContext = () => {
|
|
58
|
+
return useContext(MentionInternalContext);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// =============================================================================
|
|
62
|
+
// Provider Component
|
|
63
|
+
// =============================================================================
|
|
64
|
+
|
|
65
|
+
export namespace ComposerPrimitiveMentionRoot {
|
|
66
|
+
export type Props = {
|
|
67
|
+
children: ReactNode;
|
|
68
|
+
adapter?: Unstable_MentionAdapter | undefined;
|
|
69
|
+
/** Character(s) that trigger the mention popover. @default "@" */
|
|
70
|
+
trigger?: string | undefined;
|
|
71
|
+
/** Custom formatter for serializing/parsing mention directives. */
|
|
72
|
+
formatter?: Unstable_DirectiveFormatter | undefined;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const ComposerPrimitiveMentionRoot: FC<
|
|
77
|
+
ComposerPrimitiveMentionRoot.Props
|
|
78
|
+
> = ({
|
|
79
|
+
children,
|
|
80
|
+
adapter: adapterProp,
|
|
81
|
+
trigger: triggerChar = "@",
|
|
82
|
+
formatter: formatterProp,
|
|
83
|
+
}) => {
|
|
84
|
+
const aui = useAui();
|
|
85
|
+
const text = useAuiState((s) => s.composer.text);
|
|
86
|
+
const formatter = formatterProp ?? unstable_defaultDirectiveFormatter;
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Runtime adapter (subscribe to state changes instead of useAuiState to avoid
|
|
90
|
+
// infinite loop — getModelContext() returns a new object on every call)
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
const getRuntimeAdapter = useCallback(() => {
|
|
94
|
+
try {
|
|
95
|
+
const runtime = aui.composer().__internal_getRuntime?.();
|
|
96
|
+
return (runtime as any)?._core?.getState()?.getMentionAdapter?.();
|
|
97
|
+
} catch {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
}, [aui]);
|
|
101
|
+
const [runtimeAdapter, setRuntimeAdapter] = useState(getRuntimeAdapter);
|
|
102
|
+
useEffect(() => {
|
|
103
|
+
return aui.subscribe(() => {
|
|
104
|
+
setRuntimeAdapter((prev: unknown) => {
|
|
105
|
+
const next = getRuntimeAdapter();
|
|
106
|
+
return prev === next ? prev : next;
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}, [aui, getRuntimeAdapter]);
|
|
110
|
+
const adapter = adapterProp ?? runtimeAdapter;
|
|
111
|
+
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// Mention resource (all state + logic managed via tap primitives)
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
const mention = useResource(
|
|
117
|
+
MentionResource({ adapter, text, triggerChar, formatter, aui }),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// Internal context (stable — methods come from tapEffectEvent)
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
const internalContextValue = useMemo<MentionInternalContextValue>(
|
|
125
|
+
() => ({
|
|
126
|
+
setCursorPosition: mention.setCursorPosition,
|
|
127
|
+
registerSelectItemOverride: mention.registerSelectItemOverride,
|
|
128
|
+
}),
|
|
129
|
+
[mention.setCursorPosition, mention.registerSelectItemOverride],
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<MentionContext.Provider value={mention}>
|
|
134
|
+
<MentionInternalContext.Provider value={internalContextValue}>
|
|
135
|
+
{children}
|
|
136
|
+
</MentionInternalContext.Provider>
|
|
137
|
+
</MentionContext.Provider>
|
|
138
|
+
);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
ComposerPrimitiveMentionRoot.displayName = "ComposerPrimitive.MentionRoot";
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Primitive } from "@radix-ui/react-primitive";
|
|
4
|
+
import {
|
|
5
|
+
type ComponentRef,
|
|
6
|
+
type ComponentPropsWithoutRef,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
forwardRef,
|
|
9
|
+
useCallback,
|
|
10
|
+
} from "react";
|
|
11
|
+
import { composeEventHandlers } from "@radix-ui/primitive";
|
|
12
|
+
import { useMentionContext } from "./ComposerMentionContext";
|
|
13
|
+
import type { Unstable_MentionItem } from "@assistant-ui/core";
|
|
14
|
+
|
|
15
|
+
// =============================================================================
|
|
16
|
+
// MentionItems — Renders the list of items within a category
|
|
17
|
+
// =============================================================================
|
|
18
|
+
|
|
19
|
+
export namespace ComposerPrimitiveMentionItems {
|
|
20
|
+
export type Element = ComponentRef<typeof Primitive.div>;
|
|
21
|
+
export type Props = Omit<
|
|
22
|
+
ComponentPropsWithoutRef<typeof Primitive.div>,
|
|
23
|
+
"children"
|
|
24
|
+
> & {
|
|
25
|
+
/**
|
|
26
|
+
* Render function that receives the filtered items and returns
|
|
27
|
+
* the UI. A render-function pattern is used here (instead of a
|
|
28
|
+
* `components` prop) to give consumers full control over list layout,
|
|
29
|
+
* ordering, grouping, and empty states.
|
|
30
|
+
*/
|
|
31
|
+
children: (items: readonly Unstable_MentionItem[]) => ReactNode;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const ComposerPrimitiveMentionItems = forwardRef<
|
|
36
|
+
ComposerPrimitiveMentionItems.Element,
|
|
37
|
+
ComposerPrimitiveMentionItems.Props
|
|
38
|
+
>(({ children, ...props }, forwardedRef) => {
|
|
39
|
+
const { items, activeCategoryId, isSearchMode } = useMentionContext();
|
|
40
|
+
|
|
41
|
+
if (!activeCategoryId && !isSearchMode) return null;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<Primitive.div role="group" {...props} ref={forwardedRef}>
|
|
45
|
+
{children(items)}
|
|
46
|
+
</Primitive.div>
|
|
47
|
+
);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
ComposerPrimitiveMentionItems.displayName = "ComposerPrimitive.MentionItems";
|
|
51
|
+
|
|
52
|
+
// =============================================================================
|
|
53
|
+
// MentionItem — A single selectable mention item
|
|
54
|
+
// =============================================================================
|
|
55
|
+
|
|
56
|
+
export namespace ComposerPrimitiveMentionItem {
|
|
57
|
+
export type Element = ComponentRef<typeof Primitive.button>;
|
|
58
|
+
export type Props = ComponentPropsWithoutRef<typeof Primitive.button> & {
|
|
59
|
+
item: Unstable_MentionItem;
|
|
60
|
+
/** Position in the items list. Used for keyboard highlight matching. Falls back to findIndex by id. */
|
|
61
|
+
index?: number | undefined;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A button that inserts the mention item into the composer.
|
|
67
|
+
* Automatically receives `data-highlighted` when keyboard-navigated.
|
|
68
|
+
*/
|
|
69
|
+
export const ComposerPrimitiveMentionItem = forwardRef<
|
|
70
|
+
ComposerPrimitiveMentionItem.Element,
|
|
71
|
+
ComposerPrimitiveMentionItem.Props
|
|
72
|
+
>(({ item, index: indexProp, onClick, ...props }, forwardedRef) => {
|
|
73
|
+
const {
|
|
74
|
+
selectItem,
|
|
75
|
+
items,
|
|
76
|
+
highlightedIndex,
|
|
77
|
+
activeCategoryId,
|
|
78
|
+
isSearchMode,
|
|
79
|
+
} = useMentionContext();
|
|
80
|
+
|
|
81
|
+
const handleClick = useCallback(() => {
|
|
82
|
+
selectItem(item);
|
|
83
|
+
}, [selectItem, item]);
|
|
84
|
+
|
|
85
|
+
// Use explicit index prop if provided, fall back to findIndex
|
|
86
|
+
const itemIndex = indexProp ?? items.findIndex((i) => i.id === item.id);
|
|
87
|
+
const isHighlighted =
|
|
88
|
+
(isSearchMode || activeCategoryId !== null) &&
|
|
89
|
+
itemIndex === highlightedIndex;
|
|
90
|
+
|
|
91
|
+
return (
|
|
92
|
+
<Primitive.button
|
|
93
|
+
type="button"
|
|
94
|
+
role="option"
|
|
95
|
+
aria-selected={isHighlighted}
|
|
96
|
+
data-highlighted={isHighlighted ? "" : undefined}
|
|
97
|
+
{...props}
|
|
98
|
+
ref={forwardedRef}
|
|
99
|
+
onClick={composeEventHandlers(onClick, handleClick)}
|
|
100
|
+
/>
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
ComposerPrimitiveMentionItem.displayName = "ComposerPrimitive.MentionItem";
|