@assistant-ui/vue 0.0.0-stage → 0.0.1
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/LICENSE +21 -0
- package/README.md +90 -2
- package/dist/AuiIf.d.ts +27 -0
- package/dist/AuiIf.d.ts.map +1 -0
- package/dist/AuiIf.js +30 -0
- package/dist/AuiIf.js.map +1 -0
- package/dist/AuiProvider.d.ts +41 -0
- package/dist/AuiProvider.d.ts.map +1 -0
- package/dist/AuiProvider.js +61 -0
- package/dist/AuiProvider.js.map +1 -0
- package/dist/context.d.ts +10 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +17 -0
- package/dist/context.js.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/primitives/AttachmentByIndexProvider.d.ts +30 -0
- package/dist/primitives/AttachmentByIndexProvider.d.ts.map +1 -0
- package/dist/primitives/AttachmentByIndexProvider.js +63 -0
- package/dist/primitives/AttachmentByIndexProvider.js.map +1 -0
- package/dist/primitives/ChainOfThoughtPrimitiveAccordionTrigger.d.ts +11 -0
- package/dist/primitives/ChainOfThoughtPrimitiveAccordionTrigger.d.ts.map +1 -0
- package/dist/primitives/ChainOfThoughtPrimitiveAccordionTrigger.js +31 -0
- package/dist/primitives/ChainOfThoughtPrimitiveAccordionTrigger.js.map +1 -0
- package/dist/primitives/ChainOfThoughtPrimitiveParts.d.ts +19 -0
- package/dist/primitives/ChainOfThoughtPrimitiveParts.d.ts.map +1 -0
- package/dist/primitives/ChainOfThoughtPrimitiveParts.js +85 -0
- package/dist/primitives/ChainOfThoughtPrimitiveParts.js.map +1 -0
- package/dist/primitives/ComposerPrimitiveCancel.d.ts +12 -0
- package/dist/primitives/ComposerPrimitiveCancel.d.ts.map +1 -0
- package/dist/primitives/ComposerPrimitiveCancel.js +33 -0
- package/dist/primitives/ComposerPrimitiveCancel.js.map +1 -0
- package/dist/primitives/ComposerPrimitiveInput.d.ts +26 -0
- package/dist/primitives/ComposerPrimitiveInput.d.ts.map +1 -0
- package/dist/primitives/ComposerPrimitiveInput.js +57 -0
- package/dist/primitives/ComposerPrimitiveInput.js.map +1 -0
- package/dist/primitives/ComposerPrimitiveSend.d.ts +13 -0
- package/dist/primitives/ComposerPrimitiveSend.d.ts.map +1 -0
- package/dist/primitives/ComposerPrimitiveSend.js +31 -0
- package/dist/primitives/ComposerPrimitiveSend.js.map +1 -0
- package/dist/primitives/MessageByIdProvider.d.ts +23 -0
- package/dist/primitives/MessageByIdProvider.d.ts.map +1 -0
- package/dist/primitives/MessageByIdProvider.js +70 -0
- package/dist/primitives/MessageByIdProvider.js.map +1 -0
- package/dist/primitives/MessagePrimitiveParts.d.ts +50 -0
- package/dist/primitives/MessagePrimitiveParts.d.ts.map +1 -0
- package/dist/primitives/MessagePrimitiveParts.js +93 -0
- package/dist/primitives/MessagePrimitiveParts.js.map +1 -0
- package/dist/primitives/PartByIndexProvider.d.ts +21 -0
- package/dist/primitives/PartByIndexProvider.d.ts.map +1 -0
- package/dist/primitives/PartByIndexProvider.js +50 -0
- package/dist/primitives/PartByIndexProvider.js.map +1 -0
- package/dist/primitives/ThreadListItemPrimitiveRoot.d.ts +7 -0
- package/dist/primitives/ThreadListItemPrimitiveRoot.d.ts.map +1 -0
- package/dist/primitives/ThreadListItemPrimitiveRoot.js +36 -0
- package/dist/primitives/ThreadListItemPrimitiveRoot.js.map +1 -0
- package/dist/primitives/ThreadListItemPrimitiveTrigger.d.ts +14 -0
- package/dist/primitives/ThreadListItemPrimitiveTrigger.d.ts.map +1 -0
- package/dist/primitives/ThreadListItemPrimitiveTrigger.js +58 -0
- package/dist/primitives/ThreadListItemPrimitiveTrigger.js.map +1 -0
- package/dist/primitives/ThreadListPrimitiveRoot.d.ts +7 -0
- package/dist/primitives/ThreadListPrimitiveRoot.d.ts.map +1 -0
- package/dist/primitives/ThreadListPrimitiveRoot.js +16 -0
- package/dist/primitives/ThreadListPrimitiveRoot.js.map +1 -0
- package/dist/primitives/ThreadPrimitiveMessages.d.ts +23 -0
- package/dist/primitives/ThreadPrimitiveMessages.d.ts.map +1 -0
- package/dist/primitives/ThreadPrimitiveMessages.js +37 -0
- package/dist/primitives/ThreadPrimitiveMessages.js.map +1 -0
- package/dist/primitives/ThreadPrimitiveScrollToBottom.d.ts +25 -0
- package/dist/primitives/ThreadPrimitiveScrollToBottom.d.ts.map +1 -0
- package/dist/primitives/ThreadPrimitiveScrollToBottom.js +43 -0
- package/dist/primitives/ThreadPrimitiveScrollToBottom.js.map +1 -0
- package/dist/primitives/ThreadPrimitiveViewport.d.ts +57 -0
- package/dist/primitives/ThreadPrimitiveViewport.d.ts.map +1 -0
- package/dist/primitives/ThreadPrimitiveViewport.js +174 -0
- package/dist/primitives/ThreadPrimitiveViewport.js.map +1 -0
- package/dist/primitives/ThreadPrimitiveViewportFooter.d.ts +13 -0
- package/dist/primitives/ThreadPrimitiveViewportFooter.d.ts.map +1 -0
- package/dist/primitives/ThreadPrimitiveViewportFooter.js +46 -0
- package/dist/primitives/ThreadPrimitiveViewportFooter.js.map +1 -0
- package/dist/primitives/actionBar.d.ts +41 -0
- package/dist/primitives/actionBar.d.ts.map +1 -0
- package/dist/primitives/actionBar.js +110 -0
- package/dist/primitives/actionBar.js.map +1 -0
- package/dist/primitives/attachment.d.ts +27 -0
- package/dist/primitives/attachment.d.ts.map +1 -0
- package/dist/primitives/attachment.js +63 -0
- package/dist/primitives/attachment.js.map +1 -0
- package/dist/primitives/attrDisabled.d.ts +2 -0
- package/dist/primitives/attrDisabled.d.ts.map +1 -0
- package/dist/primitives/attrDisabled.js +6 -0
- package/dist/primitives/attrDisabled.js.map +1 -0
- package/dist/primitives/branchPicker.d.ts +18 -0
- package/dist/primitives/branchPicker.d.ts.map +1 -0
- package/dist/primitives/branchPicker.js +48 -0
- package/dist/primitives/branchPicker.js.map +1 -0
- package/dist/primitives/composerAttachments.d.ts +57 -0
- package/dist/primitives/composerAttachments.d.ts.map +1 -0
- package/dist/primitives/composerAttachments.js +132 -0
- package/dist/primitives/composerAttachments.js.map +1 -0
- package/dist/primitives/error.d.ts +15 -0
- package/dist/primitives/error.d.ts.map +1 -0
- package/dist/primitives/error.js +29 -0
- package/dist/primitives/error.js.map +1 -0
- package/dist/primitives/lastValidCache.d.ts +6 -0
- package/dist/primitives/lastValidCache.d.ts.map +1 -0
- package/dist/primitives/lastValidCache.js +9 -0
- package/dist/primitives/lastValidCache.js.map +1 -0
- package/dist/primitives/message.d.ts +12 -0
- package/dist/primitives/message.d.ts.map +1 -0
- package/dist/primitives/message.js +44 -0
- package/dist/primitives/message.js.map +1 -0
- package/dist/primitives/messageAttachments.d.ts +12 -0
- package/dist/primitives/messageAttachments.d.ts.map +1 -0
- package/dist/primitives/messageAttachments.js +27 -0
- package/dist/primitives/messageAttachments.js.map +1 -0
- package/dist/primitives/stableKeys.d.ts +5 -0
- package/dist/primitives/stableKeys.d.ts.map +1 -0
- package/dist/primitives/stableKeys.js +14 -0
- package/dist/primitives/stableKeys.js.map +1 -0
- package/dist/primitives/suggestions.d.ts +66 -0
- package/dist/primitives/suggestions.d.ts.map +1 -0
- package/dist/primitives/suggestions.js +137 -0
- package/dist/primitives/suggestions.js.map +1 -0
- package/dist/primitives/thread.d.ts +11 -0
- package/dist/primitives/thread.d.ts.map +1 -0
- package/dist/primitives/thread.js +37 -0
- package/dist/primitives/thread.js.map +1 -0
- package/dist/primitives/threadList.d.ts +82 -0
- package/dist/primitives/threadList.d.ts.map +1 -0
- package/dist/primitives/threadList.js +128 -0
- package/dist/primitives/threadList.js.map +1 -0
- package/dist/primitives/threadListFocusGroup.d.ts +14 -0
- package/dist/primitives/threadListFocusGroup.d.ts.map +1 -0
- package/dist/primitives/threadListFocusGroup.js +32 -0
- package/dist/primitives/threadListFocusGroup.js.map +1 -0
- package/dist/primitives/threadListStructural.d.ts +29 -0
- package/dist/primitives/threadListStructural.d.ts.map +1 -0
- package/dist/primitives/threadListStructural.js +55 -0
- package/dist/primitives/threadListStructural.js.map +1 -0
- package/dist/primitives/useComposerSendState.d.ts +6 -0
- package/dist/primitives/useComposerSendState.d.ts.map +1 -0
- package/dist/primitives/useComposerSendState.js +15 -0
- package/dist/primitives/useComposerSendState.js.map +1 -0
- package/dist/primitives/viewportContext.d.ts +12 -0
- package/dist/primitives/viewportContext.d.ts.map +1 -0
- package/dist/primitives/viewportContext.js +6 -0
- package/dist/primitives/viewportContext.js.map +1 -0
- package/dist/useAui.d.ts +15 -0
- package/dist/useAui.d.ts.map +1 -0
- package/dist/useAui.js +21 -0
- package/dist/useAui.js.map +1 -0
- package/dist/useAuiEvent.d.ts +17 -0
- package/dist/useAuiEvent.d.ts.map +1 -0
- package/dist/useAuiEvent.js +52 -0
- package/dist/useAuiEvent.js.map +1 -0
- package/dist/useAuiState.d.ts +23 -0
- package/dist/useAuiState.d.ts.map +1 -0
- package/dist/useAuiState.js +42 -0
- package/dist/useAuiState.js.map +1 -0
- package/dist/useScrollLock.d.ts +3 -0
- package/dist/useScrollLock.d.ts.map +1 -0
- package/dist/useScrollLock.js +53 -0
- package/dist/useScrollLock.js.map +1 -0
- package/package.json +64 -3
- package/src/AuiIf.ts +34 -0
- package/src/AuiProvider.ts +90 -0
- package/src/__tests__/AuiIf.test.ts +54 -0
- package/src/__tests__/AuiProvider.test.ts +266 -0
- package/src/__tests__/byindex-shrink-race.test.ts +411 -0
- package/src/__tests__/dist-graph.test.ts +49 -0
- package/src/__tests__/fixtures.ts +51 -0
- package/src/__tests__/primitives-actionbar.test.ts +469 -0
- package/src/__tests__/primitives-attachment.test.ts +493 -0
- package/src/__tests__/primitives-content.test.ts +749 -0
- package/src/__tests__/primitives-keyed-items.test.ts +270 -0
- package/src/__tests__/primitives-keyed-messages.test.ts +264 -0
- package/src/__tests__/primitives-structural.test.ts +573 -0
- package/src/__tests__/primitives-suggestions.test.ts +374 -0
- package/src/__tests__/primitives-threadlist.test.ts +443 -0
- package/src/__tests__/primitives-tool-ui.test.ts +579 -0
- package/src/__tests__/primitives.test.ts +361 -0
- package/src/__tests__/useAuiEvent.test.ts +159 -0
- package/src/__tests__/useAuiState.test.ts +124 -0
- package/src/context.ts +31 -0
- package/src/index.ts +87 -0
- package/src/primitives/AttachmentByIndexProvider.ts +88 -0
- package/src/primitives/ChainOfThoughtPrimitiveAccordionTrigger.test.ts +105 -0
- package/src/primitives/ChainOfThoughtPrimitiveAccordionTrigger.ts +38 -0
- package/src/primitives/ChainOfThoughtPrimitiveParts.test.ts +254 -0
- package/src/primitives/ChainOfThoughtPrimitiveParts.ts +112 -0
- package/src/primitives/ComposerPrimitiveCancel.ts +41 -0
- package/src/primitives/ComposerPrimitiveInput.ts +64 -0
- package/src/primitives/ComposerPrimitiveSend.ts +39 -0
- package/src/primitives/MessageByIdProvider.ts +89 -0
- package/src/primitives/MessagePrimitiveParts.tool-interaction.test.ts +88 -0
- package/src/primitives/MessagePrimitiveParts.ts +166 -0
- package/src/primitives/PartByIndexProvider.ts +62 -0
- package/src/primitives/ThreadListItemPrimitiveRoot.ts +57 -0
- package/src/primitives/ThreadListItemPrimitiveTrigger.ts +74 -0
- package/src/primitives/ThreadListKeyboardNav.test.ts +298 -0
- package/src/primitives/ThreadListPrimitiveRoot.ts +18 -0
- package/src/primitives/ThreadPrimitiveMessages.ts +40 -0
- package/src/primitives/ThreadPrimitiveScrollToBottom.ts +65 -0
- package/src/primitives/ThreadPrimitiveViewport.ts +233 -0
- package/src/primitives/ThreadPrimitiveViewportFooter.test.ts +305 -0
- package/src/primitives/ThreadPrimitiveViewportFooter.ts +60 -0
- package/src/primitives/actionBar.ts +159 -0
- package/src/primitives/attachment.ts +76 -0
- package/src/primitives/attrDisabled.ts +6 -0
- package/src/primitives/branchPicker.ts +77 -0
- package/src/primitives/composerAttachments.ts +181 -0
- package/src/primitives/error.ts +36 -0
- package/src/primitives/lastValidCache.ts +12 -0
- package/src/primitives/message.ts +54 -0
- package/src/primitives/messageAttachments.ts +32 -0
- package/src/primitives/stableKeys.test.ts +19 -0
- package/src/primitives/stableKeys.ts +21 -0
- package/src/primitives/suggestions.ts +179 -0
- package/src/primitives/thread.ts +46 -0
- package/src/primitives/threadList.ts +163 -0
- package/src/primitives/threadListFocusGroup.ts +56 -0
- package/src/primitives/threadListStructural.ts +86 -0
- package/src/primitives/useComposerSendState.ts +13 -0
- package/src/primitives/viewportContext.ts +16 -0
- package/src/useAui.ts +18 -0
- package/src/useAuiEvent.ts +64 -0
- package/src/useAuiState.ts +55 -0
- package/src/useScrollLock.test.ts +207 -0
- package/src/useScrollLock.ts +79 -0
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
createApp,
|
|
4
|
+
defineComponent,
|
|
5
|
+
h,
|
|
6
|
+
nextTick,
|
|
7
|
+
ref,
|
|
8
|
+
type Component,
|
|
9
|
+
} from "vue";
|
|
10
|
+
import { AuiConfig } from "@assistant-ui/store/client";
|
|
11
|
+
import { RuntimeAdapter } from "@assistant-ui/core/store";
|
|
12
|
+
import type { ExternalStoreAdapter } from "@assistant-ui/core";
|
|
13
|
+
import {
|
|
14
|
+
AssistantRuntimeImpl,
|
|
15
|
+
ExternalStoreRuntimeCore,
|
|
16
|
+
} from "@assistant-ui/core/internal";
|
|
17
|
+
import { AuiProvider } from "../AuiProvider";
|
|
18
|
+
import { ThreadListItemByIndexProvider } from "./threadList";
|
|
19
|
+
import { ThreadListItemPrimitiveRoot } from "./ThreadListItemPrimitiveRoot";
|
|
20
|
+
import { ThreadListItemPrimitiveTrigger } from "./ThreadListItemPrimitiveTrigger";
|
|
21
|
+
import { ThreadListPrimitiveRoot } from "./ThreadListPrimitiveRoot";
|
|
22
|
+
|
|
23
|
+
type Message = { id: string; role: "user"; text: string };
|
|
24
|
+
|
|
25
|
+
const createRuntime = () => {
|
|
26
|
+
let mainThreadId = "first";
|
|
27
|
+
let core!: ExternalStoreRuntimeCore;
|
|
28
|
+
const onSwitchToThread = vi.fn((threadId: string) => {
|
|
29
|
+
mainThreadId = threadId;
|
|
30
|
+
core.setAdapter(createAdapter());
|
|
31
|
+
});
|
|
32
|
+
const createAdapter = (): ExternalStoreAdapter<Message> => ({
|
|
33
|
+
messages: [],
|
|
34
|
+
convertMessage: (message) => ({
|
|
35
|
+
id: message.id,
|
|
36
|
+
role: message.role,
|
|
37
|
+
content: [{ type: "text", text: message.text }],
|
|
38
|
+
}),
|
|
39
|
+
onNew: async () => {},
|
|
40
|
+
adapters: {
|
|
41
|
+
threadList: {
|
|
42
|
+
threadId: mainThreadId,
|
|
43
|
+
threads: ["first", "second", "third"].map((id) => ({
|
|
44
|
+
status: "regular" as const,
|
|
45
|
+
id,
|
|
46
|
+
title: id,
|
|
47
|
+
})),
|
|
48
|
+
onSwitchToThread,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
core = new ExternalStoreRuntimeCore(createAdapter());
|
|
53
|
+
return {
|
|
54
|
+
runtime: new AssistantRuntimeImpl(core),
|
|
55
|
+
onSwitchToThread,
|
|
56
|
+
};
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const mount = (runtime: AssistantRuntimeImpl, view: Component) => {
|
|
60
|
+
const app = createApp(
|
|
61
|
+
defineComponent({
|
|
62
|
+
setup: () => () =>
|
|
63
|
+
h(
|
|
64
|
+
AuiProvider,
|
|
65
|
+
{ config: AuiConfig({ threads: RuntimeAdapter(runtime) }) },
|
|
66
|
+
{ default: () => h(view) },
|
|
67
|
+
),
|
|
68
|
+
}),
|
|
69
|
+
);
|
|
70
|
+
const el = document.createElement("div");
|
|
71
|
+
document.body.append(el);
|
|
72
|
+
app.mount(el);
|
|
73
|
+
return {
|
|
74
|
+
el,
|
|
75
|
+
unmount: () => {
|
|
76
|
+
app.unmount();
|
|
77
|
+
el.remove();
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const keydown = (element: HTMLElement, key: string) =>
|
|
83
|
+
element.dispatchEvent(
|
|
84
|
+
new KeyboardEvent("keydown", { bubbles: true, cancelable: true, key }),
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const buttons = (el: HTMLElement) => [
|
|
88
|
+
...el.querySelectorAll<HTMLButtonElement>("button"),
|
|
89
|
+
];
|
|
90
|
+
|
|
91
|
+
describe("thread list keyboard navigation", () => {
|
|
92
|
+
it("moves through triggers in DOM order without wrapping", async () => {
|
|
93
|
+
const { runtime } = createRuntime();
|
|
94
|
+
const View = defineComponent({
|
|
95
|
+
setup: () => () =>
|
|
96
|
+
h(
|
|
97
|
+
ThreadListPrimitiveRoot,
|
|
98
|
+
{ class: "list" },
|
|
99
|
+
{
|
|
100
|
+
default: () =>
|
|
101
|
+
[0, 1, 2].map((index) =>
|
|
102
|
+
h(
|
|
103
|
+
ThreadListItemByIndexProvider,
|
|
104
|
+
{ index, key: index },
|
|
105
|
+
{
|
|
106
|
+
default: () =>
|
|
107
|
+
h(
|
|
108
|
+
ThreadListItemPrimitiveRoot,
|
|
109
|
+
{ class: "item" },
|
|
110
|
+
{
|
|
111
|
+
default: () =>
|
|
112
|
+
h(ThreadListItemPrimitiveTrigger, null, {
|
|
113
|
+
default: () => `item ${index}`,
|
|
114
|
+
}),
|
|
115
|
+
},
|
|
116
|
+
),
|
|
117
|
+
},
|
|
118
|
+
),
|
|
119
|
+
),
|
|
120
|
+
},
|
|
121
|
+
),
|
|
122
|
+
});
|
|
123
|
+
const { el, unmount } = mount(runtime, View);
|
|
124
|
+
|
|
125
|
+
await nextTick();
|
|
126
|
+
const [first, second, third] = buttons(el);
|
|
127
|
+
const list = el.querySelector<HTMLElement>(".list")!;
|
|
128
|
+
const [firstItem, , thirdItem] = el.querySelectorAll<HTMLElement>(".item");
|
|
129
|
+
list.insertBefore(thirdItem!, firstItem!);
|
|
130
|
+
|
|
131
|
+
third!.focus();
|
|
132
|
+
expect(keydown(third!, "ArrowDown")).toBe(false);
|
|
133
|
+
expect(document.activeElement).toBe(first);
|
|
134
|
+
expect(keydown(first!, "ArrowDown")).toBe(false);
|
|
135
|
+
expect(document.activeElement).toBe(second);
|
|
136
|
+
expect(keydown(second!, "ArrowDown")).toBe(true);
|
|
137
|
+
expect(document.activeElement).toBe(second);
|
|
138
|
+
expect(keydown(first!, "ArrowUp")).toBe(false);
|
|
139
|
+
expect(document.activeElement).toBe(third);
|
|
140
|
+
expect(keydown(third!, "ArrowUp")).toBe(true);
|
|
141
|
+
expect(document.activeElement).toBe(third);
|
|
142
|
+
|
|
143
|
+
unmount();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("lets a caller veto collection navigation", async () => {
|
|
147
|
+
const { runtime } = createRuntime();
|
|
148
|
+
const View = defineComponent({
|
|
149
|
+
setup: () => () =>
|
|
150
|
+
h(ThreadListPrimitiveRoot, null, {
|
|
151
|
+
default: () =>
|
|
152
|
+
[0, 1].map((index) =>
|
|
153
|
+
h(
|
|
154
|
+
ThreadListItemByIndexProvider,
|
|
155
|
+
{ index, key: index },
|
|
156
|
+
{
|
|
157
|
+
default: () =>
|
|
158
|
+
h(
|
|
159
|
+
ThreadListItemPrimitiveRoot,
|
|
160
|
+
{
|
|
161
|
+
onKeydown: (event: KeyboardEvent) =>
|
|
162
|
+
event.preventDefault(),
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
default: () =>
|
|
166
|
+
h(ThreadListItemPrimitiveTrigger, null, {
|
|
167
|
+
default: () => `item ${index}`,
|
|
168
|
+
}),
|
|
169
|
+
},
|
|
170
|
+
),
|
|
171
|
+
},
|
|
172
|
+
),
|
|
173
|
+
),
|
|
174
|
+
}),
|
|
175
|
+
});
|
|
176
|
+
const { el, unmount } = mount(runtime, View);
|
|
177
|
+
|
|
178
|
+
await nextTick();
|
|
179
|
+
const [first] = buttons(el);
|
|
180
|
+
first!.focus();
|
|
181
|
+
expect(keydown(first!, "ArrowDown")).toBe(false);
|
|
182
|
+
expect(document.activeElement).toBe(first);
|
|
183
|
+
|
|
184
|
+
unmount();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("tracks the main thread on the item root", async () => {
|
|
188
|
+
const { runtime, onSwitchToThread } = createRuntime();
|
|
189
|
+
const View = defineComponent({
|
|
190
|
+
setup: () => () =>
|
|
191
|
+
h(ThreadListPrimitiveRoot, null, {
|
|
192
|
+
default: () =>
|
|
193
|
+
[0, 1].map((index) =>
|
|
194
|
+
h(
|
|
195
|
+
ThreadListItemByIndexProvider,
|
|
196
|
+
{ index, key: index },
|
|
197
|
+
{
|
|
198
|
+
default: () =>
|
|
199
|
+
h(
|
|
200
|
+
ThreadListItemPrimitiveRoot,
|
|
201
|
+
{ class: "item" },
|
|
202
|
+
{
|
|
203
|
+
default: () =>
|
|
204
|
+
h(ThreadListItemPrimitiveTrigger, null, {
|
|
205
|
+
default: () => `item ${index}`,
|
|
206
|
+
}),
|
|
207
|
+
},
|
|
208
|
+
),
|
|
209
|
+
},
|
|
210
|
+
),
|
|
211
|
+
),
|
|
212
|
+
}),
|
|
213
|
+
});
|
|
214
|
+
const { el, unmount } = mount(runtime, View);
|
|
215
|
+
|
|
216
|
+
await nextTick();
|
|
217
|
+
const items = el.querySelectorAll<HTMLElement>(".item");
|
|
218
|
+
expect(items[0]!.getAttribute("data-active")).toBe("true");
|
|
219
|
+
expect(items[0]!.getAttribute("aria-current")).toBe("true");
|
|
220
|
+
buttons(el)[1]!.click();
|
|
221
|
+
await vi.waitFor(() => {
|
|
222
|
+
expect(onSwitchToThread).toHaveBeenCalledWith("second");
|
|
223
|
+
expect(items[1]!.getAttribute("data-active")).toBe("true");
|
|
224
|
+
expect(items[1]!.getAttribute("aria-current")).toBe("true");
|
|
225
|
+
});
|
|
226
|
+
expect(items[0]!.hasAttribute("data-active")).toBe(false);
|
|
227
|
+
expect(items[0]!.hasAttribute("aria-current")).toBe(false);
|
|
228
|
+
|
|
229
|
+
unmount();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("keeps the trigger working without either root", async () => {
|
|
233
|
+
const { runtime, onSwitchToThread } = createRuntime();
|
|
234
|
+
const View = defineComponent({
|
|
235
|
+
setup: () => () =>
|
|
236
|
+
[0, 1].map((index) =>
|
|
237
|
+
h(
|
|
238
|
+
ThreadListItemByIndexProvider,
|
|
239
|
+
{ index, key: index },
|
|
240
|
+
{
|
|
241
|
+
default: () =>
|
|
242
|
+
h(ThreadListItemPrimitiveTrigger, null, {
|
|
243
|
+
default: () => `item ${index}`,
|
|
244
|
+
}),
|
|
245
|
+
},
|
|
246
|
+
),
|
|
247
|
+
),
|
|
248
|
+
});
|
|
249
|
+
const { el, unmount } = mount(runtime, View);
|
|
250
|
+
|
|
251
|
+
await nextTick();
|
|
252
|
+
buttons(el)[1]!.click();
|
|
253
|
+
await vi.waitFor(() => {
|
|
254
|
+
expect(onSwitchToThread).toHaveBeenCalledWith("second");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
unmount();
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("unregisters a trigger when its item unmounts", async () => {
|
|
261
|
+
const { runtime } = createRuntime();
|
|
262
|
+
const showSecond = ref(true);
|
|
263
|
+
const View = defineComponent({
|
|
264
|
+
setup: () => () =>
|
|
265
|
+
h(ThreadListPrimitiveRoot, null, {
|
|
266
|
+
default: () =>
|
|
267
|
+
[0, 1, 2]
|
|
268
|
+
.filter((index) => index !== 1 || showSecond.value)
|
|
269
|
+
.map((index) =>
|
|
270
|
+
h(
|
|
271
|
+
ThreadListItemByIndexProvider,
|
|
272
|
+
{ index, key: index },
|
|
273
|
+
{
|
|
274
|
+
default: () =>
|
|
275
|
+
h(ThreadListItemPrimitiveRoot, null, {
|
|
276
|
+
default: () =>
|
|
277
|
+
h(ThreadListItemPrimitiveTrigger, null, {
|
|
278
|
+
default: () => `item ${index}`,
|
|
279
|
+
}),
|
|
280
|
+
}),
|
|
281
|
+
},
|
|
282
|
+
),
|
|
283
|
+
),
|
|
284
|
+
}),
|
|
285
|
+
});
|
|
286
|
+
const { el, unmount } = mount(runtime, View);
|
|
287
|
+
|
|
288
|
+
await nextTick();
|
|
289
|
+
showSecond.value = false;
|
|
290
|
+
await nextTick();
|
|
291
|
+
const [first, third] = buttons(el);
|
|
292
|
+
first!.focus();
|
|
293
|
+
expect(keydown(first!, "ArrowDown")).toBe(false);
|
|
294
|
+
expect(document.activeElement).toBe(third);
|
|
295
|
+
|
|
296
|
+
unmount();
|
|
297
|
+
});
|
|
298
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineComponent,
|
|
3
|
+
h,
|
|
4
|
+
mergeProps,
|
|
5
|
+
type SlotsType,
|
|
6
|
+
type VNodeChild,
|
|
7
|
+
} from "vue";
|
|
8
|
+
import { provideThreadListCollection } from "./threadListFocusGroup";
|
|
9
|
+
|
|
10
|
+
export const ThreadListPrimitiveRoot = defineComponent({
|
|
11
|
+
name: "ThreadListPrimitiveRoot",
|
|
12
|
+
inheritAttrs: false,
|
|
13
|
+
slots: Object as SlotsType<{ default?: () => VNodeChild[] }>,
|
|
14
|
+
setup(_, { attrs, slots }) {
|
|
15
|
+
provideThreadListCollection();
|
|
16
|
+
return () => h("div", mergeProps(attrs), slots.default?.());
|
|
17
|
+
},
|
|
18
|
+
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { defineComponent, h, type SlotsType, type VNodeChild } from "vue";
|
|
2
|
+
import type {} from "@assistant-ui/core/store";
|
|
3
|
+
import { useAuiState } from "../useAuiState";
|
|
4
|
+
import { MessageByIdProvider } from "./MessageByIdProvider";
|
|
5
|
+
import { useStableKeys } from "./stableKeys";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Renders the default slot once per message in the current thread, each
|
|
9
|
+
* instance scoped to its message through {@link MessageByIdProvider} and
|
|
10
|
+
* keyed by the message id: an edit or reload that replaces the occupant of a
|
|
11
|
+
* slot remounts that row, so `<TransitionGroup>` and per-row component state
|
|
12
|
+
* follow message identity. The empty optimistic placeholder that precedes a
|
|
13
|
+
* response is its own identity, so the arrival of the real assistant message
|
|
14
|
+
* remounts that one row (a leave/enter pair under `<TransitionGroup>`).
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```html
|
|
18
|
+
* <ThreadPrimitiveMessages>
|
|
19
|
+
* <ChatMessage />
|
|
20
|
+
* </ThreadPrimitiveMessages>
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export const ThreadPrimitiveMessages = defineComponent({
|
|
24
|
+
name: "ThreadPrimitiveMessages",
|
|
25
|
+
slots: Object as SlotsType<{ default?: () => VNodeChild[] }>,
|
|
26
|
+
setup(_, { slots }) {
|
|
27
|
+
const messages = useAuiState((s) => s.thread.messages);
|
|
28
|
+
const ids = useStableKeys(() =>
|
|
29
|
+
messages.value.map((message) => message.id),
|
|
30
|
+
);
|
|
31
|
+
return () =>
|
|
32
|
+
ids.value.map((id) =>
|
|
33
|
+
h(
|
|
34
|
+
MessageByIdProvider,
|
|
35
|
+
{ id, key: id },
|
|
36
|
+
{ default: () => slots.default?.() },
|
|
37
|
+
),
|
|
38
|
+
);
|
|
39
|
+
},
|
|
40
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineComponent,
|
|
3
|
+
h,
|
|
4
|
+
inject,
|
|
5
|
+
mergeProps,
|
|
6
|
+
type PropType,
|
|
7
|
+
type SlotsType,
|
|
8
|
+
type VNodeChild,
|
|
9
|
+
} from "vue";
|
|
10
|
+
import { isDevelopment } from "@assistant-ui/core/store/internal";
|
|
11
|
+
import { isAttrDisabled } from "./attrDisabled";
|
|
12
|
+
import { viewportInjectionKey } from "./viewportContext";
|
|
13
|
+
|
|
14
|
+
let warnedOutsideViewport = false;
|
|
15
|
+
|
|
16
|
+
export const clearScrollToBottomWarningForTesting = () => {
|
|
17
|
+
warnedOutsideViewport = false;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* A button that scrolls the surrounding {@link ThreadPrimitiveViewport} to
|
|
22
|
+
* the bottom. Disabled while the viewport is already at the bottom, or when
|
|
23
|
+
* no viewport provides the channel.
|
|
24
|
+
*/
|
|
25
|
+
export const ThreadPrimitiveScrollToBottom = defineComponent({
|
|
26
|
+
name: "ThreadPrimitiveScrollToBottom",
|
|
27
|
+
inheritAttrs: false,
|
|
28
|
+
props: {
|
|
29
|
+
behavior: {
|
|
30
|
+
type: String as PropType<ScrollBehavior>,
|
|
31
|
+
default: "auto",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
slots: Object as SlotsType<{ default?: () => VNodeChild[] }>,
|
|
35
|
+
setup(props, { attrs, slots }) {
|
|
36
|
+
const viewport = inject(viewportInjectionKey, null);
|
|
37
|
+
if (isDevelopment && !viewport && !warnedOutsideViewport) {
|
|
38
|
+
warnedOutsideViewport = true;
|
|
39
|
+
console.warn(
|
|
40
|
+
"ThreadPrimitiveScrollToBottom: no surrounding ThreadPrimitiveViewport provides the scroll channel; the button stays disabled. Place it inside the viewport.",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
const onClick = (event: MouseEvent) => {
|
|
44
|
+
if (
|
|
45
|
+
event.defaultPrevented ||
|
|
46
|
+
!viewport ||
|
|
47
|
+
viewport.isAtBottom.value ||
|
|
48
|
+
isAttrDisabled(attrs)
|
|
49
|
+
)
|
|
50
|
+
return;
|
|
51
|
+
viewport.scrollToBottom(props.behavior);
|
|
52
|
+
};
|
|
53
|
+
return () =>
|
|
54
|
+
h(
|
|
55
|
+
"button",
|
|
56
|
+
mergeProps(attrs, {
|
|
57
|
+
type: "button",
|
|
58
|
+
disabled:
|
|
59
|
+
!viewport || viewport.isAtBottom.value || isAttrDisabled(attrs),
|
|
60
|
+
onClick,
|
|
61
|
+
}),
|
|
62
|
+
slots.default?.(),
|
|
63
|
+
);
|
|
64
|
+
},
|
|
65
|
+
});
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineComponent,
|
|
3
|
+
h,
|
|
4
|
+
onMounted,
|
|
5
|
+
onScopeDispose,
|
|
6
|
+
provide,
|
|
7
|
+
shallowRef,
|
|
8
|
+
watch,
|
|
9
|
+
type SlotsType,
|
|
10
|
+
type VNodeChild,
|
|
11
|
+
} from "vue";
|
|
12
|
+
import type {} from "@assistant-ui/core/store";
|
|
13
|
+
import { useAuiEvent } from "../useAuiEvent";
|
|
14
|
+
import { useAuiState } from "../useAuiState";
|
|
15
|
+
import {
|
|
16
|
+
isUserScrollUp,
|
|
17
|
+
isViewportAtBottom,
|
|
18
|
+
observeContentResize,
|
|
19
|
+
viewportOverflows,
|
|
20
|
+
} from "@assistant-ui/store/client";
|
|
21
|
+
import { viewportInjectionKey } from "./viewportContext";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A scrollable container that keeps the thread pinned to the bottom: content
|
|
25
|
+
* growth scrolls back down while the user sits at the bottom, a run start
|
|
26
|
+
* scrolls down, and scrolling up unpins until the user returns to the bottom.
|
|
27
|
+
* The four options mirror the React hook and are independent: `autoScroll`
|
|
28
|
+
* covers follow-on-content-growth, and the other three gate the
|
|
29
|
+
* first-messages, run-start, and thread-switch scrolls. Provides the
|
|
30
|
+
* scroll-to-bottom channel that {@link ThreadPrimitiveScrollToBottom} drives;
|
|
31
|
+
* the React viewport's top-anchor system stays in the React viewport store
|
|
32
|
+
* and is not ported.
|
|
33
|
+
*/
|
|
34
|
+
export const ThreadPrimitiveViewport = defineComponent({
|
|
35
|
+
name: "ThreadPrimitiveViewport",
|
|
36
|
+
props: {
|
|
37
|
+
autoScroll: {
|
|
38
|
+
type: Boolean,
|
|
39
|
+
default: true,
|
|
40
|
+
},
|
|
41
|
+
scrollToBottomOnInitialize: {
|
|
42
|
+
type: Boolean,
|
|
43
|
+
default: true,
|
|
44
|
+
},
|
|
45
|
+
scrollToBottomOnRunStart: {
|
|
46
|
+
type: Boolean,
|
|
47
|
+
default: true,
|
|
48
|
+
},
|
|
49
|
+
scrollToBottomOnThreadSwitch: {
|
|
50
|
+
type: Boolean,
|
|
51
|
+
default: true,
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
slots: Object as SlotsType<{ default?: () => VNodeChild[] }>,
|
|
55
|
+
setup(props, { slots }) {
|
|
56
|
+
const divRef = shallowRef<HTMLElement | null>(null);
|
|
57
|
+
const contentInset = shallowRef(0);
|
|
58
|
+
const contentInsetEntries = new Map<symbol, number>();
|
|
59
|
+
let intent: ScrollBehavior | null = null;
|
|
60
|
+
const isAtBottom = shallowRef(true);
|
|
61
|
+
let lastScrollTop = 0;
|
|
62
|
+
let lastScrollHeight = 0;
|
|
63
|
+
let lastObservedScrollHeight = 0;
|
|
64
|
+
let lastObservedClientHeight = 0;
|
|
65
|
+
let frame: number | null = null;
|
|
66
|
+
|
|
67
|
+
const scrollToBottom = (behavior: ScrollBehavior) => {
|
|
68
|
+
const div = divRef.value;
|
|
69
|
+
if (!div) return;
|
|
70
|
+
intent = behavior;
|
|
71
|
+
div.scrollTo?.({ top: div.scrollHeight, behavior });
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const scheduleScrollToBottom = (behavior: ScrollBehavior) => {
|
|
75
|
+
intent = behavior;
|
|
76
|
+
// The immediate watch below runs synchronously during SSR, where no
|
|
77
|
+
// frame scheduler exists.
|
|
78
|
+
if (typeof requestAnimationFrame === "undefined") return;
|
|
79
|
+
if (frame !== null) cancelAnimationFrame(frame);
|
|
80
|
+
frame = requestAnimationFrame(() => {
|
|
81
|
+
frame = null;
|
|
82
|
+
scrollToBottom(behavior);
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const handleScroll = () => {
|
|
87
|
+
const div = divRef.value;
|
|
88
|
+
if (!div) return;
|
|
89
|
+
|
|
90
|
+
const newIsAtBottom = isViewportAtBottom(div, contentInset.value);
|
|
91
|
+
const inFlightDownward = !newIsAtBottom && lastScrollTop < div.scrollTop;
|
|
92
|
+
if (!inFlightDownward) {
|
|
93
|
+
if (newIsAtBottom) {
|
|
94
|
+
// At-bottom is ambiguous while the viewport does not overflow; keep
|
|
95
|
+
// the intent alive until content can actually scroll.
|
|
96
|
+
if (viewportOverflows(div, contentInset.value)) intent = null;
|
|
97
|
+
} else if (
|
|
98
|
+
isUserScrollUp(
|
|
99
|
+
{ scrollTop: lastScrollTop, scrollHeight: lastScrollHeight },
|
|
100
|
+
div,
|
|
101
|
+
)
|
|
102
|
+
) {
|
|
103
|
+
intent = null;
|
|
104
|
+
}
|
|
105
|
+
if (newIsAtBottom || intent === null) isAtBottom.value = newIsAtBottom;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
lastScrollTop = div.scrollTop;
|
|
109
|
+
lastScrollHeight = div.scrollHeight;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const updateContentInset = () => {
|
|
113
|
+
let total = 0;
|
|
114
|
+
for (const height of contentInsetEntries.values()) total += height;
|
|
115
|
+
if (contentInset.value === total) return;
|
|
116
|
+
const grew = total > contentInset.value;
|
|
117
|
+
contentInset.value = total;
|
|
118
|
+
// A growing inset obscures content a pinned viewport was showing, so it
|
|
119
|
+
// follows like a content resize; a shrinking inset reveals content and
|
|
120
|
+
// must not move the viewport.
|
|
121
|
+
if (grew) {
|
|
122
|
+
if (intent) {
|
|
123
|
+
scrollToBottom(intent);
|
|
124
|
+
} else if (props.autoScroll && isAtBottom.value) {
|
|
125
|
+
scrollToBottom("instant");
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
handleScroll();
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const registerContentInset = () => {
|
|
132
|
+
const id = Symbol();
|
|
133
|
+
contentInsetEntries.set(id, 0);
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
setHeight: (height: number) => {
|
|
137
|
+
if (contentInsetEntries.get(id) === height) return;
|
|
138
|
+
contentInsetEntries.set(id, height);
|
|
139
|
+
updateContentInset();
|
|
140
|
+
},
|
|
141
|
+
unregister: () => {
|
|
142
|
+
if (!contentInsetEntries.delete(id)) return;
|
|
143
|
+
updateContentInset();
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const onContentResize = () => {
|
|
149
|
+
const div = divRef.value;
|
|
150
|
+
if (!div) return;
|
|
151
|
+
const { scrollHeight, clientHeight } = div;
|
|
152
|
+
if (
|
|
153
|
+
scrollHeight === lastObservedScrollHeight &&
|
|
154
|
+
clientHeight === lastObservedClientHeight
|
|
155
|
+
) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
lastObservedScrollHeight = scrollHeight;
|
|
159
|
+
lastObservedClientHeight = clientHeight;
|
|
160
|
+
|
|
161
|
+
if (intent) {
|
|
162
|
+
scrollToBottom(intent);
|
|
163
|
+
} else if (props.autoScroll && isAtBottom.value) {
|
|
164
|
+
scrollToBottom("instant");
|
|
165
|
+
}
|
|
166
|
+
handleScroll();
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// A pointer gesture invalidates pending bottom-scroll intent; otherwise an
|
|
170
|
+
// intent kept alive by a non-overflowing thread hijacks the next content
|
|
171
|
+
// growth. Unlike the React hook, an already scheduled frame is cancelled
|
|
172
|
+
// too, so the gesture also wins the race against a just-planted intent.
|
|
173
|
+
const onPointerdown = () => {
|
|
174
|
+
intent = null;
|
|
175
|
+
if (frame !== null) {
|
|
176
|
+
cancelAnimationFrame(frame);
|
|
177
|
+
frame = null;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
let disconnect: (() => void) | undefined;
|
|
182
|
+
onMounted(() => {
|
|
183
|
+
const div = divRef.value;
|
|
184
|
+
if (!div) return;
|
|
185
|
+
disconnect = observeContentResize(div, onContentResize);
|
|
186
|
+
});
|
|
187
|
+
onScopeDispose(() => {
|
|
188
|
+
disconnect?.();
|
|
189
|
+
if (frame !== null) cancelAnimationFrame(frame);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
const hasMessages = useAuiState((s) => s.thread.messages.length > 0);
|
|
193
|
+
let initialized = false;
|
|
194
|
+
watch(
|
|
195
|
+
[hasMessages, () => props.scrollToBottomOnInitialize],
|
|
196
|
+
([has, enabled]) => {
|
|
197
|
+
if (!has) {
|
|
198
|
+
initialized = false;
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
if (!enabled || initialized) return;
|
|
202
|
+
initialized = true;
|
|
203
|
+
if (intent !== null) return;
|
|
204
|
+
scheduleScrollToBottom("instant");
|
|
205
|
+
},
|
|
206
|
+
{ immediate: true },
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
useAuiEvent("thread.runStart", () => {
|
|
210
|
+
if (!props.scrollToBottomOnRunStart) return;
|
|
211
|
+
scheduleScrollToBottom("auto");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
useAuiEvent("threads.selectionChanged", () => {
|
|
215
|
+
if (!props.scrollToBottomOnThreadSwitch) return;
|
|
216
|
+
scheduleScrollToBottom("instant");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
provide(viewportInjectionKey, {
|
|
220
|
+
isAtBottom,
|
|
221
|
+
scrollToBottom: (behavior: ScrollBehavior = "auto") =>
|
|
222
|
+
scrollToBottom(behavior),
|
|
223
|
+
registerContentInset,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
return () =>
|
|
227
|
+
h(
|
|
228
|
+
"div",
|
|
229
|
+
{ ref: divRef, onScroll: handleScroll, onPointerdown },
|
|
230
|
+
slots.default?.(),
|
|
231
|
+
);
|
|
232
|
+
},
|
|
233
|
+
});
|