@assistant-ui/react 0.14.27 → 0.14.29
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/client/ExternalThread.d.ts +11 -1
- package/dist/client/ExternalThread.d.ts.map +1 -1
- package/dist/client/ExternalThread.js +758 -513
- package/dist/client/ExternalThread.js.map +1 -1
- package/dist/context/react/utils/createContextStoreHook.d.ts.map +1 -1
- package/dist/context/react/utils/createStateHookForRuntime.d.ts.map +1 -1
- package/dist/legacy-runtime/AssistantRuntimeProvider.js.map +1 -1
- package/dist/legacy-runtime/cloud/auiV0.d.ts +12 -1
- package/dist/legacy-runtime/cloud/auiV0.d.ts.map +1 -1
- package/dist/legacy-runtime/cloud/auiV0.js +2 -1
- package/dist/legacy-runtime/cloud/auiV0.js.map +1 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.d.ts +1 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.d.ts.map +1 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.js +7 -6
- package/dist/legacy-runtime/runtime-cores/assistant-transport/commandQueue.js.map +1 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.d.ts.map +1 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.js +30 -15
- package/dist/legacy-runtime/runtime-cores/assistant-transport/runManager.js.map +1 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.d.ts.map +1 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js +8 -4
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.js.map +1 -1
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.js +1 -0
- package/dist/legacy-runtime/runtime-cores/assistant-transport/useConvertedState.js.map +1 -1
- package/dist/mcp-apps/McpAppRenderer.d.ts.map +1 -1
- package/dist/model-context/makeAssistantVisible.d.ts.map +1 -1
- package/dist/model-context/makeAssistantVisible.js +7 -1
- package/dist/model-context/makeAssistantVisible.js.map +1 -1
- package/dist/primitives/actionBar/ActionBarCopy.d.ts.map +1 -1
- package/dist/primitives/actionBar/ActionBarEdit.d.ts.map +1 -1
- package/dist/primitives/actionBar/ActionBarExportMarkdown.d.ts.map +1 -1
- package/dist/primitives/actionBar/ActionBarFeedbackNegative.d.ts.map +1 -1
- package/dist/primitives/actionBar/ActionBarFeedbackPositive.d.ts.map +1 -1
- package/dist/primitives/actionBar/ActionBarReload.d.ts.map +1 -1
- package/dist/primitives/actionBar/ActionBarRoot.d.ts.map +1 -1
- package/dist/primitives/actionBar/ActionBarRoot.js +3 -3
- package/dist/primitives/actionBar/ActionBarRoot.js.map +1 -1
- package/dist/primitives/actionBar/ActionBarSpeak.d.ts.map +1 -1
- package/dist/primitives/actionBar/ActionBarStopSpeaking.d.ts.map +1 -1
- package/dist/primitives/actionBar/useActionBarFloatStatus.d.ts +7 -6
- package/dist/primitives/actionBar/useActionBarFloatStatus.d.ts.map +1 -1
- package/dist/primitives/actionBar/useActionBarFloatStatus.js +10 -11
- package/dist/primitives/actionBar/useActionBarFloatStatus.js.map +1 -1
- package/dist/primitives/actionBarMore/ActionBarMoreContent.d.ts.map +1 -1
- package/dist/primitives/actionBarMore/ActionBarMoreContent.js.map +1 -1
- package/dist/primitives/actionBarMore/ActionBarMoreItem.d.ts.map +1 -1
- package/dist/primitives/actionBarMore/ActionBarMoreSeparator.d.ts.map +1 -1
- package/dist/primitives/actionBarMore/ActionBarMoreTrigger.d.ts.map +1 -1
- package/dist/primitives/assistantModal/AssistantModalAnchor.d.ts.map +1 -1
- package/dist/primitives/assistantModal/AssistantModalContent.d.ts.map +1 -1
- package/dist/primitives/assistantModal/AssistantModalContent.js.map +1 -1
- package/dist/primitives/assistantModal/AssistantModalTrigger.d.ts.map +1 -1
- package/dist/primitives/assistantModal/popoverRenderPrimitives.d.ts.map +1 -1
- package/dist/primitives/attachment/AttachmentRemove.d.ts.map +1 -1
- package/dist/primitives/attachment/AttachmentRemove.js +1 -0
- package/dist/primitives/attachment/AttachmentRemove.js.map +1 -1
- package/dist/primitives/attachment/AttachmentRoot.d.ts.map +1 -1
- package/dist/primitives/attachment/AttachmentThumb.d.ts.map +1 -1
- package/dist/primitives/attachment/AttachmentThumb.js.map +1 -1
- package/dist/primitives/branchPicker/BranchPickerNext.d.ts.map +1 -1
- package/dist/primitives/branchPicker/BranchPickerPrevious.d.ts.map +1 -1
- package/dist/primitives/branchPicker/BranchPickerRoot.d.ts.map +1 -1
- package/dist/primitives/branchPicker/BranchPickerRoot.js.map +1 -1
- package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.d.ts.map +1 -1
- package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.js +1 -0
- package/dist/primitives/chainOfThought/ChainOfThoughtAccordionTrigger.js.map +1 -1
- package/dist/primitives/chainOfThought/ChainOfThoughtRoot.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerAddAttachment.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerAddAttachment.js +8 -2
- package/dist/primitives/composer/ComposerAddAttachment.js.map +1 -1
- package/dist/primitives/composer/ComposerAttachmentDropzone.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerAttachmentDropzone.js +1 -3
- package/dist/primitives/composer/ComposerAttachmentDropzone.js.map +1 -1
- package/dist/primitives/composer/ComposerCancel.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerDictate.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerDictationTranscript.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerDictationTranscript.js.map +1 -1
- package/dist/primitives/composer/ComposerInput.js +6 -4
- package/dist/primitives/composer/ComposerInput.js.map +1 -1
- package/dist/primitives/composer/ComposerQuote.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerQuote.js.map +1 -1
- package/dist/primitives/composer/ComposerRoot.d.ts +5 -0
- package/dist/primitives/composer/ComposerRoot.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerRoot.js +86 -40
- package/dist/primitives/composer/ComposerRoot.js.map +1 -1
- package/dist/primitives/composer/ComposerSend.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerSend.js +1 -0
- package/dist/primitives/composer/ComposerSend.js.map +1 -1
- package/dist/primitives/composer/ComposerStopDictation.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerStopDictation.js +1 -0
- package/dist/primitives/composer/ComposerStopDictation.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopover.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopover.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverBack.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverCategories.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverCategories.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverItems.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverItems.js.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/TriggerPopoverRootContext.js.map +1 -1
- package/dist/primitives/composer/trigger/detectTrigger.d.ts +1 -1
- package/dist/primitives/composer/trigger/triggerDetectionResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/triggerKeyboardResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/triggerNavigationResource.d.ts.map +1 -1
- package/dist/primitives/composer/trigger/triggerSelectionResource.d.ts.map +1 -1
- package/dist/primitives/dropdownMenuRenderPrimitives.d.ts.map +1 -1
- package/dist/primitives/error/ErrorMessage.d.ts.map +1 -1
- package/dist/primitives/error/ErrorMessage.js.map +1 -1
- package/dist/primitives/error/ErrorRoot.d.ts.map +1 -1
- package/dist/primitives/message/MessageParts.js.map +1 -1
- package/dist/primitives/message/MessagePartsGrouped.js.map +1 -1
- package/dist/primitives/message/MessageRoot.d.ts.map +1 -1
- package/dist/primitives/messagePart/MessagePartImage.d.ts.map +1 -1
- package/dist/primitives/messagePart/MessagePartText.d.ts.map +1 -1
- package/dist/primitives/messagePart/MessagePartText.js.map +1 -1
- package/dist/primitives/messagePart/useMessagePartData.d.ts +3 -2
- package/dist/primitives/messagePart/useMessagePartData.d.ts.map +1 -1
- package/dist/primitives/messagePart/useMessagePartData.js +3 -2
- package/dist/primitives/messagePart/useMessagePartData.js.map +1 -1
- package/dist/primitives/messagePart/useMessagePartFile.d.ts +5 -4
- package/dist/primitives/messagePart/useMessagePartFile.d.ts.map +1 -1
- package/dist/primitives/messagePart/useMessagePartFile.js +11 -3
- package/dist/primitives/messagePart/useMessagePartFile.js.map +1 -1
- package/dist/primitives/messagePart/useMessagePartImage.d.ts +5 -4
- package/dist/primitives/messagePart/useMessagePartImage.d.ts.map +1 -1
- package/dist/primitives/messagePart/useMessagePartImage.js +10 -3
- package/dist/primitives/messagePart/useMessagePartImage.js.map +1 -1
- package/dist/primitives/messagePart/useMessagePartReasoning.d.ts +5 -4
- package/dist/primitives/messagePart/useMessagePartReasoning.d.ts.map +1 -1
- package/dist/primitives/messagePart/useMessagePartReasoning.js +10 -3
- package/dist/primitives/messagePart/useMessagePartReasoning.js.map +1 -1
- package/dist/primitives/messagePart/useMessagePartSource.d.ts +6 -4
- package/dist/primitives/messagePart/useMessagePartSource.d.ts.map +1 -1
- package/dist/primitives/messagePart/useMessagePartSource.js +12 -3
- package/dist/primitives/messagePart/useMessagePartSource.js.map +1 -1
- package/dist/primitives/messagePart/useMessagePartText.d.ts +6 -5
- package/dist/primitives/messagePart/useMessagePartText.d.ts.map +1 -1
- package/dist/primitives/messagePart/useMessagePartText.js +10 -3
- package/dist/primitives/messagePart/useMessagePartText.js.map +1 -1
- package/dist/primitives/queueItem/QueueItemRemove.d.ts.map +1 -1
- package/dist/primitives/queueItem/QueueItemRemove.js +1 -0
- package/dist/primitives/queueItem/QueueItemRemove.js.map +1 -1
- package/dist/primitives/queueItem/QueueItemSteer.d.ts.map +1 -1
- package/dist/primitives/queueItem/QueueItemSteer.js +1 -0
- package/dist/primitives/queueItem/QueueItemSteer.js.map +1 -1
- package/dist/primitives/queueItem/QueueItemText.d.ts.map +1 -1
- package/dist/primitives/queueItem/QueueItemText.js.map +1 -1
- package/dist/primitives/selectionToolbar/SelectionToolbarQuote.d.ts.map +1 -1
- package/dist/primitives/selectionToolbar/SelectionToolbarRoot.d.ts.map +1 -1
- package/dist/primitives/selectionToolbar/SelectionToolbarRoot.js.map +1 -1
- package/dist/primitives/suggestion/SuggestionDescription.d.ts.map +1 -1
- package/dist/primitives/suggestion/SuggestionDescription.js.map +1 -1
- package/dist/primitives/suggestion/SuggestionTitle.d.ts.map +1 -1
- package/dist/primitives/suggestion/SuggestionTitle.js.map +1 -1
- package/dist/primitives/suggestion/SuggestionTrigger.d.ts.map +1 -1
- package/dist/primitives/suggestion/SuggestionTrigger.js +1 -0
- package/dist/primitives/suggestion/SuggestionTrigger.js.map +1 -1
- package/dist/primitives/thread/ThreadRoot.d.ts.map +1 -1
- package/dist/primitives/thread/ThreadScrollToBottom.d.ts.map +1 -1
- package/dist/primitives/thread/ThreadScrollToBottom.js +1 -0
- package/dist/primitives/thread/ThreadScrollToBottom.js.map +1 -1
- package/dist/primitives/thread/ThreadSuggestion.d.ts.map +1 -1
- package/dist/primitives/thread/ThreadViewport.d.ts.map +1 -1
- package/dist/primitives/thread/ThreadViewport.js +53 -19
- package/dist/primitives/thread/ThreadViewport.js.map +1 -1
- package/dist/primitives/thread/ThreadViewportFooter.d.ts.map +1 -1
- package/dist/primitives/thread/topAnchor/createReserveObservers.d.ts.map +1 -1
- package/dist/primitives/thread/topAnchor/mountTopAnchorReserve.d.ts +4 -0
- package/dist/primitives/thread/topAnchor/mountTopAnchorReserve.d.ts.map +1 -1
- package/dist/primitives/thread/topAnchor/mountTopAnchorReserve.js +14 -1
- package/dist/primitives/thread/topAnchor/mountTopAnchorReserve.js.map +1 -1
- package/dist/primitives/thread/topAnchor/topAnchorTurn.d.ts +13 -1
- package/dist/primitives/thread/topAnchor/topAnchorTurn.d.ts.map +1 -1
- package/dist/primitives/thread/topAnchor/topAnchorTurn.js +14 -1
- package/dist/primitives/thread/topAnchor/topAnchorTurn.js.map +1 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.js +2 -1
- package/dist/primitives/thread/useThreadViewportAutoScroll.js.map +1 -1
- package/dist/primitives/threadList/ThreadListLoadMore.d.ts.map +1 -1
- package/dist/primitives/threadList/ThreadListNew.d.ts.map +1 -1
- package/dist/primitives/threadList/ThreadListRoot.d.ts.map +1 -1
- package/dist/primitives/threadList/ThreadListRoot.js.map +1 -1
- package/dist/primitives/threadListFocusGroup.d.ts.map +1 -1
- package/dist/primitives/threadListItem/ThreadListItemArchive.d.ts.map +1 -1
- package/dist/primitives/threadListItem/ThreadListItemDelete.d.ts.map +1 -1
- package/dist/primitives/threadListItem/ThreadListItemRoot.d.ts.map +1 -1
- package/dist/primitives/threadListItem/ThreadListItemRoot.js.map +1 -1
- package/dist/primitives/threadListItem/ThreadListItemTrigger.d.ts.map +1 -1
- package/dist/primitives/threadListItem/ThreadListItemTrigger.js.map +1 -1
- package/dist/primitives/threadListItem/ThreadListItemUnarchive.d.ts.map +1 -1
- package/dist/primitives/threadListItemMore/ThreadListItemMoreContent.d.ts.map +1 -1
- package/dist/primitives/threadListItemMore/ThreadListItemMoreContent.js.map +1 -1
- package/dist/primitives/threadListItemMore/ThreadListItemMoreItem.d.ts.map +1 -1
- package/dist/primitives/threadListItemMore/ThreadListItemMoreRoot.js.map +1 -1
- package/dist/primitives/threadListItemMore/ThreadListItemMoreSeparator.d.ts.map +1 -1
- package/dist/primitives/threadListItemMore/ThreadListItemMoreTrigger.d.ts.map +1 -1
- package/dist/tests/setup.d.ts +1 -1
- package/dist/utils/Primitive.d.ts.map +1 -1
- package/dist/utils/Primitive.js.map +1 -1
- package/dist/utils/createActionButton.d.ts.map +1 -1
- package/dist/utils/hooks/useOnResizeContent.js +1 -0
- package/dist/utils/hooks/useOnResizeContent.js.map +1 -1
- package/dist/utils/hooks/useSizeHandle.js +1 -0
- package/dist/utils/hooks/useSizeHandle.js.map +1 -1
- package/dist/utils/smooth/useSmooth.d.ts.map +1 -1
- package/dist/utils/smooth/useSmooth.js +2 -2
- package/dist/utils/smooth/useSmooth.js.map +1 -1
- package/package.json +20 -20
- package/src/client/ExternalThread.ts +341 -53
- package/src/legacy-runtime/cloud/auiV0.ts +14 -0
- package/src/legacy-runtime/runtime-cores/assistant-transport/commandQueue.ts +4 -1
- package/src/legacy-runtime/runtime-cores/assistant-transport/runManager.ts +37 -10
- package/src/legacy-runtime/runtime-cores/assistant-transport/transport-delivery.test.tsx +79 -0
- package/src/legacy-runtime/runtime-cores/assistant-transport/transport-parentid.test.tsx +97 -0
- package/src/legacy-runtime/runtime-cores/assistant-transport/transport-scheduling.test.ts +113 -2
- package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransport.spec.md +10 -2
- package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.test.tsx +189 -0
- package/src/legacy-runtime/runtime-cores/assistant-transport/useAssistantTransportRuntime.ts +20 -4
- package/src/model-context/makeAssistantVisible.test.tsx +146 -0
- package/src/model-context/makeAssistantVisible.tsx +18 -1
- package/src/primitives/actionBar/useActionBarFloatStatus.ts +7 -5
- package/src/primitives/composer/ComposerAddAttachment.test.tsx +108 -0
- package/src/primitives/composer/ComposerAddAttachment.ts +10 -4
- package/src/primitives/composer/ComposerAttachmentDropzone.test.tsx +3 -2
- package/src/primitives/composer/ComposerAttachmentDropzone.tsx +3 -2
- package/src/primitives/composer/ComposerInput.test.tsx +98 -1
- package/src/primitives/composer/ComposerInput.tsx +10 -8
- package/src/primitives/composer/ComposerRoot.test.tsx +226 -5
- package/src/primitives/composer/ComposerRoot.tsx +50 -1
- package/src/primitives/messagePart/useMessagePartData.ts +3 -2
- package/src/primitives/messagePart/useMessagePartFile.ts +19 -7
- package/src/primitives/messagePart/useMessagePartImage.ts +18 -7
- package/src/primitives/messagePart/useMessagePartReasoning.ts +15 -6
- package/src/primitives/messagePart/useMessagePartSource.ts +20 -7
- package/src/primitives/messagePart/useMessagePartText.ts +15 -5
- package/src/primitives/thread/ThreadViewport.tsx +15 -0
- package/src/primitives/thread/topAnchor/mountTopAnchorReserve.test.ts +243 -0
- package/src/primitives/thread/topAnchor/mountTopAnchorReserve.ts +28 -7
- package/src/primitives/thread/topAnchor/topAnchorTurn.test.ts +69 -0
- package/src/primitives/thread/topAnchor/topAnchorTurn.ts +29 -0
- package/src/primitives/thread/useThreadViewportAutoScroll.test.tsx +86 -1
- package/src/primitives/thread/useThreadViewportAutoScroll.ts +6 -1
- package/src/tests/DataRenderers.test.tsx +212 -0
- package/src/tests/RemoteThreadListRuntime.adapterProvider.test.tsx +1 -11
- package/src/tests/external-thread-attachments.test.tsx +372 -0
- package/src/tests/external-thread-parity.test.tsx +232 -0
- package/src/tests/generative-ui.test.tsx +1 -46
- package/src/tests/messagePartSwitchRace.test.tsx +172 -0
- package/src/tests/messagePartTypeMismatch.test.tsx +146 -0
- package/src/tests/threadMessageById.test.tsx +0 -7
- package/src/utils/smooth/useSmooth.ts +7 -4
- package/src/tests/BaseComposerRuntimeCore.test.ts +0 -612
- package/src/tests/BaseSubscribable.test.ts +0 -54
- package/src/tests/DataRenderers.test.ts +0 -355
- package/src/tests/ExternalStoreThreadRuntimeCore.test.ts +0 -685
- package/src/tests/LocalThreadRuntimeCore.test.ts +0 -324
- package/src/tests/auiV0Encode.test.ts +0 -188
- package/src/tests/external-message-converter.test.ts +0 -346
- package/src/tests/shouldContinue.test.ts +0 -122
|
@@ -9,6 +9,7 @@ import { ComposerPrimitiveInput } from "./ComposerInput";
|
|
|
9
9
|
const setText = vi.fn<(text: string) => void>();
|
|
10
10
|
const setCursorPosition = vi.fn<(pos: number) => void>();
|
|
11
11
|
const sendSpy = vi.fn<(options?: { steer?: boolean }) => void>();
|
|
12
|
+
const addAttachment = vi.fn<(file: File) => Promise<void>>();
|
|
12
13
|
|
|
13
14
|
const composerState = {
|
|
14
15
|
isEditing: true,
|
|
@@ -42,7 +43,7 @@ vi.mock("@assistant-ui/store", () => {
|
|
|
42
43
|
getState: () => composerState,
|
|
43
44
|
cancel: () => {},
|
|
44
45
|
send: sendSpy,
|
|
45
|
-
addAttachment
|
|
46
|
+
addAttachment,
|
|
46
47
|
}),
|
|
47
48
|
thread: () => ({
|
|
48
49
|
getState: () => threadState,
|
|
@@ -140,6 +141,16 @@ const fireKeyDown = (
|
|
|
140
141
|
return event;
|
|
141
142
|
};
|
|
142
143
|
|
|
144
|
+
const firePaste = (textarea: HTMLTextAreaElement, files: File[]) => {
|
|
145
|
+
const event = new Event("paste", { bubbles: true, cancelable: true });
|
|
146
|
+
Object.defineProperty(event, "clipboardData", {
|
|
147
|
+
value: { files },
|
|
148
|
+
configurable: true,
|
|
149
|
+
});
|
|
150
|
+
textarea.dispatchEvent(event);
|
|
151
|
+
return event;
|
|
152
|
+
};
|
|
153
|
+
|
|
143
154
|
const setMatchMedia = (matches: boolean) => {
|
|
144
155
|
Object.defineProperty(window, "matchMedia", {
|
|
145
156
|
writable: true,
|
|
@@ -169,6 +180,7 @@ describe("ComposerPrimitiveInput", () => {
|
|
|
169
180
|
setText.mockReset();
|
|
170
181
|
setCursorPosition.mockReset();
|
|
171
182
|
sendSpy.mockReset();
|
|
183
|
+
addAttachment.mockReset();
|
|
172
184
|
composerState.isEditing = true;
|
|
173
185
|
composerState.text = "";
|
|
174
186
|
composerState.isEmpty = true;
|
|
@@ -501,4 +513,89 @@ describe("ComposerPrimitiveInput", () => {
|
|
|
501
513
|
expect(event.defaultPrevented).toBe(true);
|
|
502
514
|
});
|
|
503
515
|
});
|
|
516
|
+
|
|
517
|
+
describe("paste attachments", () => {
|
|
518
|
+
it("attempts every pasted file and prevents default when attachments are supported", async () => {
|
|
519
|
+
threadState.capabilities = { queue: false, attachments: true };
|
|
520
|
+
|
|
521
|
+
const textarea = await mount();
|
|
522
|
+
|
|
523
|
+
let event!: Event;
|
|
524
|
+
await act(async () => {
|
|
525
|
+
event = firePaste(textarea, [
|
|
526
|
+
new File(["a"], "first.txt", { type: "text/plain" }),
|
|
527
|
+
new File(["b"], "second.txt", { type: "text/plain" }),
|
|
528
|
+
new File(["c"], "third.txt", { type: "text/plain" }),
|
|
529
|
+
]);
|
|
530
|
+
await Promise.resolve();
|
|
531
|
+
await Promise.resolve();
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
expect(addAttachment).toHaveBeenCalledTimes(3);
|
|
535
|
+
expect(addAttachment.mock.calls.map(([file]) => file.name)).toEqual([
|
|
536
|
+
"first.txt",
|
|
537
|
+
"second.txt",
|
|
538
|
+
"third.txt",
|
|
539
|
+
]);
|
|
540
|
+
expect(event.defaultPrevented).toBe(true);
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
it("continues processing other files when one attachment fails without console.error", async () => {
|
|
544
|
+
threadState.capabilities = { queue: false, attachments: true };
|
|
545
|
+
const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
546
|
+
|
|
547
|
+
addAttachment
|
|
548
|
+
.mockRejectedValueOnce(new Error("upload failed"))
|
|
549
|
+
.mockResolvedValueOnce(undefined)
|
|
550
|
+
.mockResolvedValueOnce(undefined);
|
|
551
|
+
|
|
552
|
+
const textarea = await mount();
|
|
553
|
+
|
|
554
|
+
await act(async () => {
|
|
555
|
+
firePaste(textarea, [
|
|
556
|
+
new File(["a"], "first.txt", { type: "text/plain" }),
|
|
557
|
+
new File(["b"], "second.txt", { type: "text/plain" }),
|
|
558
|
+
new File(["c"], "third.txt", { type: "text/plain" }),
|
|
559
|
+
]);
|
|
560
|
+
await Promise.resolve();
|
|
561
|
+
await Promise.resolve();
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
expect(addAttachment).toHaveBeenCalledTimes(3);
|
|
565
|
+
expect(errorSpy).not.toHaveBeenCalled();
|
|
566
|
+
errorSpy.mockRestore();
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it("does not add attachments on paste when attachments are unsupported", async () => {
|
|
570
|
+
threadState.capabilities = { queue: false, attachments: false };
|
|
571
|
+
|
|
572
|
+
const textarea = await mount();
|
|
573
|
+
|
|
574
|
+
let event!: Event;
|
|
575
|
+
await act(async () => {
|
|
576
|
+
event = firePaste(textarea, [
|
|
577
|
+
new File(["a"], "first.txt", { type: "text/plain" }),
|
|
578
|
+
]);
|
|
579
|
+
await Promise.resolve();
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
expect(addAttachment).not.toHaveBeenCalled();
|
|
583
|
+
expect(event.defaultPrevented).toBe(false);
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
it("skips attachment handling when addAttachmentOnPaste is false", async () => {
|
|
587
|
+
threadState.capabilities = { queue: false, attachments: true };
|
|
588
|
+
|
|
589
|
+
const textarea = await mount({ addAttachmentOnPaste: false });
|
|
590
|
+
|
|
591
|
+
await act(async () => {
|
|
592
|
+
firePaste(textarea, [
|
|
593
|
+
new File(["a"], "first.txt", { type: "text/plain" }),
|
|
594
|
+
]);
|
|
595
|
+
await Promise.resolve();
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
expect(addAttachment).not.toHaveBeenCalled();
|
|
599
|
+
});
|
|
600
|
+
});
|
|
504
601
|
});
|
|
@@ -271,14 +271,16 @@ export const ComposerPrimitiveInput = forwardRef<
|
|
|
271
271
|
const files = Array.from(e.clipboardData?.files || []);
|
|
272
272
|
|
|
273
273
|
if (threadCapabilities.attachments && files.length > 0) {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
274
|
+
e.preventDefault();
|
|
275
|
+
await Promise.all(
|
|
276
|
+
files.map(async (file) => {
|
|
277
|
+
try {
|
|
278
|
+
await aui.composer().addAttachment(file);
|
|
279
|
+
} catch {
|
|
280
|
+
// The composer runtime emits composer.attachmentAddError before rejecting.
|
|
281
|
+
}
|
|
282
|
+
}),
|
|
283
|
+
);
|
|
282
284
|
}
|
|
283
285
|
};
|
|
284
286
|
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @vitest-environment jsdom
|
|
3
|
-
*/
|
|
1
|
+
/** @vitest-environment jsdom */
|
|
4
2
|
import { act } from "react";
|
|
5
|
-
import type { ReactNode } from "react";
|
|
3
|
+
import type { MouseEvent, ReactNode } from "react";
|
|
4
|
+
import { createPortal } from "react-dom";
|
|
6
5
|
import { createRoot } from "react-dom/client";
|
|
7
6
|
import type { Root } from "react-dom/client";
|
|
8
7
|
import type * as AssistantStore from "@assistant-ui/store";
|
|
9
8
|
import type * as ComposerSendModule from "./ComposerSend";
|
|
9
|
+
import { fireEvent, render } from "@testing-library/react";
|
|
10
10
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
11
11
|
import { useComposerCompactContextOptional } from "./ComposerCompactContext";
|
|
12
12
|
import type { ComposerCompactContextValue } from "./ComposerCompactContext";
|
|
@@ -58,7 +58,228 @@ const resetStoreState = () => {
|
|
|
58
58
|
storeState.composer.text = "";
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
-
describe("ComposerPrimitiveRoot", () => {
|
|
61
|
+
describe("ComposerPrimitiveRoot click-to-focus", () => {
|
|
62
|
+
beforeEach(() => {
|
|
63
|
+
resetStoreState();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const setupComposer = (props?: ComposerPrimitiveRoot.Props) => {
|
|
67
|
+
const { getByTestId } = render(
|
|
68
|
+
<ComposerPrimitiveRoot {...props}>
|
|
69
|
+
<div data-testid="blank-space">
|
|
70
|
+
<textarea data-testid="composer-input" />
|
|
71
|
+
</div>
|
|
72
|
+
</ComposerPrimitiveRoot>,
|
|
73
|
+
);
|
|
74
|
+
return {
|
|
75
|
+
blankSpace: getByTestId("blank-space"),
|
|
76
|
+
textarea: getByTestId("composer-input"),
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
it("focuses the textarea when primary-button mousedown starts on blank form space", () => {
|
|
81
|
+
const { blankSpace, textarea } = setupComposer();
|
|
82
|
+
|
|
83
|
+
fireEvent.mouseDown(blankSpace, { button: 0 });
|
|
84
|
+
|
|
85
|
+
expect(document.activeElement).toBe(textarea);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("does not move focus to the textarea when mousedown starts on an interactive child", () => {
|
|
89
|
+
const { getByTestId } = render(
|
|
90
|
+
<ComposerPrimitiveRoot>
|
|
91
|
+
<button data-testid="interactive-child" type="button">
|
|
92
|
+
Action
|
|
93
|
+
</button>
|
|
94
|
+
<textarea data-testid="composer-input" />
|
|
95
|
+
</ComposerPrimitiveRoot>,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const notPrevented = fireEvent.mouseDown(getByTestId("interactive-child"), {
|
|
99
|
+
button: 0,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
expect(notPrevented).toBe(true);
|
|
103
|
+
expect(document.activeElement).not.toBe(getByTestId("composer-input"));
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("does not steal focus from a role-based widget carrying a tabindex", () => {
|
|
107
|
+
const { getByTestId } = render(
|
|
108
|
+
<ComposerPrimitiveRoot>
|
|
109
|
+
<div role="slider" tabIndex={0} data-testid="widget" />
|
|
110
|
+
<textarea data-testid="composer-input" />
|
|
111
|
+
</ComposerPrimitiveRoot>,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
const notPrevented = fireEvent.mouseDown(getByTestId("widget"), {
|
|
115
|
+
button: 0,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
expect(notPrevented).toBe(true);
|
|
119
|
+
expect(document.activeElement).not.toBe(getByTestId("composer-input"));
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("does not steal focus from a roving-tabindex child with tabindex=-1", () => {
|
|
123
|
+
const { getByTestId } = render(
|
|
124
|
+
<ComposerPrimitiveRoot>
|
|
125
|
+
<span tabIndex={-1} data-testid="roving-item">
|
|
126
|
+
Item
|
|
127
|
+
</span>
|
|
128
|
+
<textarea data-testid="composer-input" />
|
|
129
|
+
</ComposerPrimitiveRoot>,
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
const notPrevented = fireEvent.mouseDown(getByTestId("roving-item"), {
|
|
133
|
+
button: 0,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
expect(notPrevented).toBe(true);
|
|
137
|
+
expect(document.activeElement).not.toBe(getByTestId("composer-input"));
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("does not move focus to the textarea on non-primary mousedown", () => {
|
|
141
|
+
const { blankSpace, textarea } = setupComposer();
|
|
142
|
+
|
|
143
|
+
const notPrevented = fireEvent.mouseDown(blankSpace, { button: 2 });
|
|
144
|
+
|
|
145
|
+
expect(notPrevented).toBe(true);
|
|
146
|
+
expect(document.activeElement).not.toBe(textarea);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("focuses a contenteditable composer target when primary-button mousedown starts on blank form space", () => {
|
|
150
|
+
const { getByTestId } = render(
|
|
151
|
+
<ComposerPrimitiveRoot>
|
|
152
|
+
<div data-testid="blank-space">
|
|
153
|
+
<div contentEditable data-testid="composer-input" tabIndex={0} />
|
|
154
|
+
</div>
|
|
155
|
+
</ComposerPrimitiveRoot>,
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
fireEvent.mouseDown(getByTestId("blank-space"), { button: 0 });
|
|
159
|
+
|
|
160
|
+
expect(document.activeElement).toBe(getByTestId("composer-input"));
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("still focuses the textarea when the form is hosted inside a tabindex=-1 wrapper", () => {
|
|
164
|
+
const { getByTestId } = render(
|
|
165
|
+
<div tabIndex={-1}>
|
|
166
|
+
<ComposerPrimitiveRoot>
|
|
167
|
+
<div data-testid="blank-space">
|
|
168
|
+
<textarea data-testid="composer-input" />
|
|
169
|
+
</div>
|
|
170
|
+
</ComposerPrimitiveRoot>
|
|
171
|
+
</div>,
|
|
172
|
+
);
|
|
173
|
+
|
|
174
|
+
fireEvent.mouseDown(getByTestId("blank-space"), { button: 0 });
|
|
175
|
+
|
|
176
|
+
expect(document.activeElement).toBe(getByTestId("composer-input"));
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it("ignores mousedown on portaled descendants rendered outside the form's DOM", () => {
|
|
180
|
+
const { getByTestId } = render(
|
|
181
|
+
<ComposerPrimitiveRoot>
|
|
182
|
+
{createPortal(<div data-testid="portal-blank" />, document.body)}
|
|
183
|
+
<textarea data-testid="composer-input" />
|
|
184
|
+
</ComposerPrimitiveRoot>,
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const notPrevented = fireEvent.mouseDown(getByTestId("portal-blank"), {
|
|
188
|
+
button: 0,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
expect(notPrevented).toBe(true);
|
|
192
|
+
expect(document.activeElement).not.toBe(getByTestId("composer-input"));
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("keeps the mousedown default when the only textarea is disabled", () => {
|
|
196
|
+
const { getByTestId } = render(
|
|
197
|
+
<ComposerPrimitiveRoot>
|
|
198
|
+
<div data-testid="blank-space">
|
|
199
|
+
<textarea disabled data-testid="composer-input" />
|
|
200
|
+
</div>
|
|
201
|
+
</ComposerPrimitiveRoot>,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const notPrevented = fireEvent.mouseDown(getByTestId("blank-space"), {
|
|
205
|
+
button: 0,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
expect(notPrevented).toBe(true);
|
|
209
|
+
expect(document.activeElement).not.toBe(getByTestId("composer-input"));
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("skips a disabled textarea and focuses the next composer input", () => {
|
|
213
|
+
const { getByTestId } = render(
|
|
214
|
+
<ComposerPrimitiveRoot>
|
|
215
|
+
<div data-testid="blank-space">
|
|
216
|
+
<textarea disabled data-testid="disabled-input" />
|
|
217
|
+
<div contentEditable data-testid="composer-input" tabIndex={0} />
|
|
218
|
+
</div>
|
|
219
|
+
</ComposerPrimitiveRoot>,
|
|
220
|
+
);
|
|
221
|
+
|
|
222
|
+
fireEvent.mouseDown(getByTestId("blank-space"), { button: 0 });
|
|
223
|
+
|
|
224
|
+
expect(document.activeElement).toBe(getByTestId("composer-input"));
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("keeps the mousedown default when the form contains no composer input", () => {
|
|
228
|
+
const { getByTestId } = render(
|
|
229
|
+
<ComposerPrimitiveRoot>
|
|
230
|
+
<div data-testid="blank-space" />
|
|
231
|
+
</ComposerPrimitiveRoot>,
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
const notPrevented = fireEvent.mouseDown(getByTestId("blank-space"), {
|
|
235
|
+
button: 0,
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
expect(notPrevented).toBe(true);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it("lets descendants keep native mousedown defaults via stopPropagation", () => {
|
|
242
|
+
const { getByTestId } = render(
|
|
243
|
+
<ComposerPrimitiveRoot>
|
|
244
|
+
<div data-testid="selectable" onMouseDown={(e) => e.stopPropagation()}>
|
|
245
|
+
Selectable content
|
|
246
|
+
</div>
|
|
247
|
+
<textarea data-testid="composer-input" />
|
|
248
|
+
</ComposerPrimitiveRoot>,
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const notPrevented = fireEvent.mouseDown(getByTestId("selectable"), {
|
|
252
|
+
button: 0,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
expect(notPrevented).toBe(true);
|
|
256
|
+
expect(document.activeElement).not.toBe(getByTestId("composer-input"));
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("lets consumer onMouseDown prevent the default focus behavior", () => {
|
|
260
|
+
const onMouseDown = vi.fn((event: MouseEvent<HTMLFormElement>) => {
|
|
261
|
+
event.preventDefault();
|
|
262
|
+
});
|
|
263
|
+
const { blankSpace, textarea } = setupComposer({ onMouseDown });
|
|
264
|
+
|
|
265
|
+
fireEvent.mouseDown(blankSpace, { button: 0 });
|
|
266
|
+
|
|
267
|
+
expect(onMouseDown).toHaveBeenCalledTimes(1);
|
|
268
|
+
expect(document.activeElement).not.toBe(textarea);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("runs consumer onMouseDown and preserves default focus behavior when it does not prevent default", () => {
|
|
272
|
+
const onMouseDown = vi.fn();
|
|
273
|
+
const { blankSpace, textarea } = setupComposer({ onMouseDown });
|
|
274
|
+
|
|
275
|
+
fireEvent.mouseDown(blankSpace, { button: 0 });
|
|
276
|
+
|
|
277
|
+
expect(onMouseDown).toHaveBeenCalledTimes(1);
|
|
278
|
+
expect(document.activeElement).toBe(textarea);
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe("ComposerPrimitiveRoot compact mode", () => {
|
|
62
283
|
let container: HTMLDivElement;
|
|
63
284
|
let root: Root;
|
|
64
285
|
|
|
@@ -8,12 +8,36 @@ import {
|
|
|
8
8
|
type FormEvent,
|
|
9
9
|
forwardRef,
|
|
10
10
|
type ComponentPropsWithoutRef,
|
|
11
|
+
type MouseEvent,
|
|
11
12
|
useMemo,
|
|
12
13
|
useState,
|
|
13
14
|
} from "react";
|
|
14
15
|
import { useComposerSend } from "./ComposerSend";
|
|
15
16
|
import { ComposerCompactContext } from "./ComposerCompactContext";
|
|
16
17
|
|
|
18
|
+
const CONTENT_EDITABLE_SELECTOR =
|
|
19
|
+
"[contenteditable]:not([contenteditable='false'])";
|
|
20
|
+
|
|
21
|
+
const COMPOSER_INPUT_SELECTOR = `textarea:not(:disabled), ${CONTENT_EDITABLE_SELECTOR}`;
|
|
22
|
+
|
|
23
|
+
// Keeps tabindex="-1": roving-tabindex widgets click-focus items at -1.
|
|
24
|
+
const INTERACTIVE_ELEMENT_SELECTOR = [
|
|
25
|
+
"a",
|
|
26
|
+
"audio[controls]",
|
|
27
|
+
"button",
|
|
28
|
+
"details",
|
|
29
|
+
"embed",
|
|
30
|
+
"iframe",
|
|
31
|
+
"input",
|
|
32
|
+
"label",
|
|
33
|
+
"select",
|
|
34
|
+
"summary",
|
|
35
|
+
"textarea",
|
|
36
|
+
"video[controls]",
|
|
37
|
+
CONTENT_EDITABLE_SELECTOR,
|
|
38
|
+
"[tabindex]",
|
|
39
|
+
].join(", ");
|
|
40
|
+
|
|
17
41
|
export namespace ComposerPrimitiveRoot {
|
|
18
42
|
export type Element = ComponentRef<typeof Primitive.form>;
|
|
19
43
|
/**
|
|
@@ -40,6 +64,11 @@ export namespace ComposerPrimitiveRoot {
|
|
|
40
64
|
* is submitted (e.g., via Enter key or submit button). It automatically prevents the
|
|
41
65
|
* default form submission and triggers the composer's send functionality.
|
|
42
66
|
*
|
|
67
|
+
* Clicking blank form space focuses the composer input (the first textarea or
|
|
68
|
+
* contenteditable inside the form). Text selection cannot start from blank
|
|
69
|
+
* areas; descendants that need native mousedown defaults can call
|
|
70
|
+
* `stopPropagation()` in their own `onMouseDown`.
|
|
71
|
+
*
|
|
43
72
|
* @example
|
|
44
73
|
* ```tsx
|
|
45
74
|
* <ComposerPrimitive.Root>
|
|
@@ -51,7 +80,7 @@ export namespace ComposerPrimitiveRoot {
|
|
|
51
80
|
export const ComposerPrimitiveRoot = forwardRef<
|
|
52
81
|
ComposerPrimitiveRoot.Element,
|
|
53
82
|
ComposerPrimitiveRoot.Props
|
|
54
|
-
>(({ onSubmit, compact, ...rest }, forwardedRef) => {
|
|
83
|
+
>(({ onSubmit, onMouseDown, compact, ...rest }, forwardedRef) => {
|
|
55
84
|
const send = useComposerSend();
|
|
56
85
|
|
|
57
86
|
const [multiline, setMultiline] = useState(false);
|
|
@@ -74,6 +103,25 @@ export const ComposerPrimitiveRoot = forwardRef<
|
|
|
74
103
|
send();
|
|
75
104
|
};
|
|
76
105
|
|
|
106
|
+
const handleMouseDown = (e: MouseEvent<HTMLFormElement>) => {
|
|
107
|
+
if (e.button !== 0) return;
|
|
108
|
+
const target = e.target;
|
|
109
|
+
// Portaled descendants (e.g. a dialog overlay) propagate mousedown through
|
|
110
|
+
// the React tree while their DOM nodes live outside the form.
|
|
111
|
+
if (!(target instanceof Element) || !e.currentTarget.contains(target))
|
|
112
|
+
return;
|
|
113
|
+
// Bound the ancestor walk to the form: hosts like AssistantModal render
|
|
114
|
+
// the composer inside a wrapper carrying tabindex="-1" (Radix FocusScope).
|
|
115
|
+
const interactive = target.closest(INTERACTIVE_ELEMENT_SELECTOR);
|
|
116
|
+
if (interactive && e.currentTarget.contains(interactive)) return;
|
|
117
|
+
const input = e.currentTarget.querySelector<HTMLElement>(
|
|
118
|
+
COMPOSER_INPUT_SELECTOR,
|
|
119
|
+
);
|
|
120
|
+
if (!input) return;
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
input.focus();
|
|
123
|
+
};
|
|
124
|
+
|
|
77
125
|
return (
|
|
78
126
|
<ComposerCompactContext.Provider value={compact ? compactContext : null}>
|
|
79
127
|
<Primitive.form
|
|
@@ -81,6 +129,7 @@ export const ComposerPrimitiveRoot = forwardRef<
|
|
|
81
129
|
data-compact={isCompact ? "" : undefined}
|
|
82
130
|
ref={forwardedRef}
|
|
83
131
|
onSubmit={composeEventHandlers(onSubmit, handleSubmit)}
|
|
132
|
+
onMouseDown={composeEventHandlers(onMouseDown, handleMouseDown)}
|
|
84
133
|
/>
|
|
85
134
|
</ComposerCompactContext.Provider>
|
|
86
135
|
);
|
|
@@ -5,8 +5,9 @@ import { useAuiState } from "@assistant-ui/store";
|
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* @deprecated Use {@link useAuiState} to select and narrow `s.part`.
|
|
8
|
-
* Return `null` for optional rendering
|
|
9
|
-
*
|
|
8
|
+
* Return `null` for optional rendering. Do not throw inside the selector:
|
|
9
|
+
* selectors run inside `useSyncExternalStore`'s `getSnapshot`, so a transient
|
|
10
|
+
* part mismatch during thread switches can unmount the React root.
|
|
10
11
|
*
|
|
11
12
|
* @example
|
|
12
13
|
* ```tsx
|
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
FileMessagePart,
|
|
5
|
+
MessagePartState,
|
|
6
|
+
MessagePartStatus,
|
|
7
|
+
} from "@assistant-ui/core";
|
|
4
8
|
import { useAuiState } from "@assistant-ui/store";
|
|
5
9
|
|
|
10
|
+
const COMPLETE_STATUS: MessagePartStatus = Object.freeze({ type: "complete" });
|
|
11
|
+
|
|
12
|
+
const EMPTY_FILE_PART: MessagePartState & FileMessagePart = Object.freeze({
|
|
13
|
+
type: "file",
|
|
14
|
+
data: "",
|
|
15
|
+
mimeType: "",
|
|
16
|
+
status: COMPLETE_STATUS,
|
|
17
|
+
});
|
|
18
|
+
|
|
6
19
|
/**
|
|
7
20
|
* @deprecated Use {@link useAuiState} to select and narrow `s.part`.
|
|
8
|
-
* Return `null` for optional rendering
|
|
9
|
-
*
|
|
21
|
+
* Return `null` for optional rendering. Do not throw inside the selector:
|
|
22
|
+
* selectors run inside `useSyncExternalStore`'s `getSnapshot`, so a transient
|
|
23
|
+
* part mismatch during thread switches can unmount the React root.
|
|
10
24
|
*
|
|
11
25
|
* @example
|
|
12
26
|
* ```tsx
|
|
@@ -19,11 +33,9 @@ import { useAuiState } from "@assistant-ui/store";
|
|
|
19
33
|
* See the {@link https://assistant-ui.com/docs/migrations/v0-12 migration guide}.
|
|
20
34
|
*/
|
|
21
35
|
export const useMessagePartFile = () => {
|
|
36
|
+
// Sentinel instead of throw: see useMessagePartText for the invariant.
|
|
22
37
|
const file = useAuiState((s) => {
|
|
23
|
-
if (s.part.type !== "file")
|
|
24
|
-
throw new Error(
|
|
25
|
-
"MessagePartFile can only be used inside file message parts.",
|
|
26
|
-
);
|
|
38
|
+
if (s.part.type !== "file") return EMPTY_FILE_PART;
|
|
27
39
|
|
|
28
40
|
return s.part as MessagePartState & FileMessagePart;
|
|
29
41
|
});
|
|
@@ -1,12 +1,25 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
ImageMessagePart,
|
|
5
|
+
MessagePartState,
|
|
6
|
+
MessagePartStatus,
|
|
7
|
+
} from "@assistant-ui/core";
|
|
4
8
|
import { useAuiState } from "@assistant-ui/store";
|
|
5
9
|
|
|
10
|
+
const COMPLETE_STATUS: MessagePartStatus = Object.freeze({ type: "complete" });
|
|
11
|
+
|
|
12
|
+
const EMPTY_IMAGE_PART: MessagePartState & ImageMessagePart = Object.freeze({
|
|
13
|
+
type: "image",
|
|
14
|
+
image: "",
|
|
15
|
+
status: COMPLETE_STATUS,
|
|
16
|
+
});
|
|
17
|
+
|
|
6
18
|
/**
|
|
7
19
|
* @deprecated Use {@link useAuiState} to select and narrow `s.part`.
|
|
8
|
-
* Return `null` for optional rendering
|
|
9
|
-
*
|
|
20
|
+
* Return `null` for optional rendering. Do not throw inside the selector:
|
|
21
|
+
* selectors run inside `useSyncExternalStore`'s `getSnapshot`, so a transient
|
|
22
|
+
* part mismatch during thread switches can unmount the React root.
|
|
10
23
|
*
|
|
11
24
|
* @example
|
|
12
25
|
* ```tsx
|
|
@@ -19,11 +32,9 @@ import { useAuiState } from "@assistant-ui/store";
|
|
|
19
32
|
* See the {@link https://assistant-ui.com/docs/migrations/v0-12 migration guide}.
|
|
20
33
|
*/
|
|
21
34
|
export const useMessagePartImage = () => {
|
|
35
|
+
// Sentinel instead of throw: see useMessagePartText for the invariant.
|
|
22
36
|
const image = useAuiState((s) => {
|
|
23
|
-
if (s.part.type !== "image")
|
|
24
|
-
throw new Error(
|
|
25
|
-
"MessagePartImage can only be used inside image message parts.",
|
|
26
|
-
);
|
|
37
|
+
if (s.part.type !== "image") return EMPTY_IMAGE_PART;
|
|
27
38
|
|
|
28
39
|
return s.part as MessagePartState & ImageMessagePart;
|
|
29
40
|
});
|
|
@@ -3,13 +3,24 @@
|
|
|
3
3
|
import type {
|
|
4
4
|
ReasoningMessagePart,
|
|
5
5
|
MessagePartState,
|
|
6
|
+
MessagePartStatus,
|
|
6
7
|
} from "@assistant-ui/core";
|
|
7
8
|
import { useAuiState } from "@assistant-ui/store";
|
|
8
9
|
|
|
10
|
+
const COMPLETE_STATUS: MessagePartStatus = Object.freeze({ type: "complete" });
|
|
11
|
+
|
|
12
|
+
const EMPTY_REASONING_PART: MessagePartState & ReasoningMessagePart =
|
|
13
|
+
Object.freeze({
|
|
14
|
+
type: "reasoning",
|
|
15
|
+
text: "",
|
|
16
|
+
status: COMPLETE_STATUS,
|
|
17
|
+
});
|
|
18
|
+
|
|
9
19
|
/**
|
|
10
20
|
* @deprecated Use {@link useAuiState} to select and narrow `s.part`.
|
|
11
|
-
* Return `null` for optional rendering
|
|
12
|
-
*
|
|
21
|
+
* Return `null` for optional rendering. Do not throw inside the selector:
|
|
22
|
+
* selectors run inside `useSyncExternalStore`'s `getSnapshot`, so a transient
|
|
23
|
+
* part mismatch during thread switches can unmount the React root.
|
|
13
24
|
*
|
|
14
25
|
* @example
|
|
15
26
|
* ```tsx
|
|
@@ -22,11 +33,9 @@ import { useAuiState } from "@assistant-ui/store";
|
|
|
22
33
|
* See the {@link https://assistant-ui.com/docs/migrations/v0-12 migration guide}.
|
|
23
34
|
*/
|
|
24
35
|
export const useMessagePartReasoning = () => {
|
|
36
|
+
// Sentinel instead of throw: see useMessagePartText for the invariant.
|
|
25
37
|
const text = useAuiState((s) => {
|
|
26
|
-
if (s.part.type !== "reasoning")
|
|
27
|
-
throw new Error(
|
|
28
|
-
"MessagePartReasoning can only be used inside reasoning message parts.",
|
|
29
|
-
);
|
|
38
|
+
if (s.part.type !== "reasoning") return EMPTY_REASONING_PART;
|
|
30
39
|
|
|
31
40
|
return s.part as MessagePartState & ReasoningMessagePart;
|
|
32
41
|
});
|
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
SourceMessagePart,
|
|
5
|
+
MessagePartState,
|
|
6
|
+
MessagePartStatus,
|
|
7
|
+
} from "@assistant-ui/core";
|
|
4
8
|
import { useAuiState } from "@assistant-ui/store";
|
|
5
9
|
|
|
10
|
+
const COMPLETE_STATUS: MessagePartStatus = Object.freeze({ type: "complete" });
|
|
11
|
+
|
|
12
|
+
const EMPTY_SOURCE_PART: MessagePartState & SourceMessagePart = Object.freeze({
|
|
13
|
+
type: "source",
|
|
14
|
+
sourceType: "url",
|
|
15
|
+
id: "",
|
|
16
|
+
url: "",
|
|
17
|
+
status: COMPLETE_STATUS,
|
|
18
|
+
});
|
|
19
|
+
|
|
6
20
|
/**
|
|
7
21
|
* @deprecated Use {@link useAuiState} to select and narrow `s.part`.
|
|
8
|
-
* Return `null` for optional rendering
|
|
9
|
-
*
|
|
22
|
+
* Return `null` for optional rendering. Do not throw inside the selector:
|
|
23
|
+
* selectors run inside `useSyncExternalStore`'s `getSnapshot`, so a transient
|
|
24
|
+
* part mismatch during thread switches can unmount the React root.
|
|
10
25
|
*
|
|
11
26
|
* @example
|
|
12
27
|
* ```tsx
|
|
@@ -19,11 +34,9 @@ import { useAuiState } from "@assistant-ui/store";
|
|
|
19
34
|
* See the {@link https://assistant-ui.com/docs/migrations/v0-12 migration guide}.
|
|
20
35
|
*/
|
|
21
36
|
export const useMessagePartSource = () => {
|
|
37
|
+
// Sentinel instead of throw: see useMessagePartText for the invariant.
|
|
22
38
|
const source = useAuiState((s) => {
|
|
23
|
-
if (s.part.type !== "source")
|
|
24
|
-
throw new Error(
|
|
25
|
-
"MessagePartSource can only be used inside source message parts.",
|
|
26
|
-
);
|
|
39
|
+
if (s.part.type !== "source") return EMPTY_SOURCE_PART;
|
|
27
40
|
|
|
28
41
|
return s.part as MessagePartState & SourceMessagePart;
|
|
29
42
|
});
|