@agent-native/toolkit 0.211.0 → 0.212.0-nightly-20261021010049
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/agent-native.eject.json +1 -0
- package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ar-SA.js +23 -0
- package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
- package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/de-DE.js +23 -0
- package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
- package/dist/app/i18n/catalogs/en-US.d.ts +23 -0
- package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/en-US.js +23 -0
- package/dist/app/i18n/catalogs/en-US.js.map +1 -1
- package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/es-ES.js +23 -0
- package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
- package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/fr-FR.js +23 -0
- package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
- package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/hi-IN.js +23 -0
- package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
- package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ja-JP.js +23 -0
- package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
- package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ko-KR.js +23 -0
- package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
- package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/pt-BR.js +23 -0
- package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
- package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/zh-CN.js +23 -0
- package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
- package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/zh-TW.js +23 -0
- package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
- package/dist/app/sharing/OwnershipFilter.d.ts +8 -0
- package/dist/app/sharing/OwnershipFilter.d.ts.map +1 -0
- package/dist/app/sharing/OwnershipFilter.js +20 -0
- package/dist/app/sharing/OwnershipFilter.js.map +1 -0
- package/dist/app/sharing/ShareButton.d.ts +43 -1
- package/dist/app/sharing/ShareButton.d.ts.map +1 -1
- package/dist/app/sharing/ShareButton.js +102 -63
- package/dist/app/sharing/ShareButton.js.map +1 -1
- package/dist/app/sharing/ShareDialog.d.ts +3 -1
- package/dist/app/sharing/ShareDialog.d.ts.map +1 -1
- package/dist/app/sharing/ShareDialog.js +18 -12
- package/dist/app/sharing/ShareDialog.js.map +1 -1
- package/dist/app/sharing/ShareVisibilityConfirmDialog.d.ts +20 -0
- package/dist/app/sharing/ShareVisibilityConfirmDialog.d.ts.map +1 -0
- package/dist/app/sharing/ShareVisibilityConfirmDialog.js +57 -0
- package/dist/app/sharing/ShareVisibilityConfirmDialog.js.map +1 -0
- package/dist/app/sharing/VisibilityBadge.d.ts +8 -1
- package/dist/app/sharing/VisibilityBadge.d.ts.map +1 -1
- package/dist/app/sharing/VisibilityBadge.js +16 -1
- package/dist/app/sharing/VisibilityBadge.js.map +1 -1
- package/dist/app/sharing/index.d.ts +2 -0
- package/dist/app/sharing/index.d.ts.map +1 -1
- package/dist/app/sharing/index.js +2 -0
- package/dist/app/sharing/index.js.map +1 -1
- package/dist/chat-history/ChatHistoryList.d.ts +19 -0
- package/dist/chat-history/ChatHistoryList.d.ts.map +1 -1
- package/dist/chat-history/ChatHistoryList.js +19 -7
- package/dist/chat-history/ChatHistoryList.js.map +1 -1
- package/dist/chat-history/ChatHistoryRail.d.ts +55 -8
- package/dist/chat-history/ChatHistoryRail.d.ts.map +1 -1
- package/dist/chat-history/ChatHistoryRail.js +33 -7
- package/dist/chat-history/ChatHistoryRail.js.map +1 -1
- package/dist/chat-history/index.d.ts +3 -3
- package/dist/chat-history/index.d.ts.map +1 -1
- package/dist/chat-history/index.js +1 -1
- package/dist/chat-history/index.js.map +1 -1
- package/dist/chat-history/useChatHistoryRailController.d.ts +44 -0
- package/dist/chat-history/useChatHistoryRailController.d.ts.map +1 -1
- package/dist/chat-history/useChatHistoryRailController.js +46 -0
- package/dist/chat-history/useChatHistoryRailController.js.map +1 -1
- package/dist/chat-history.css +29 -0
- package/dist/composer/TiptapComposer.d.ts +7 -0
- package/dist/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/composer/TiptapComposer.js +15 -2
- package/dist/composer/TiptapComposer.js.map +1 -1
- package/dist/sharing/OwnershipFilter.d.ts +21 -0
- package/dist/sharing/OwnershipFilter.d.ts.map +1 -0
- package/dist/sharing/OwnershipFilter.js +30 -0
- package/dist/sharing/OwnershipFilter.js.map +1 -0
- package/dist/sharing/VisibilityBadge.d.ts +12 -2
- package/dist/sharing/VisibilityBadge.d.ts.map +1 -1
- package/dist/sharing/VisibilityBadge.js +13 -4
- package/dist/sharing/VisibilityBadge.js.map +1 -1
- package/dist/sharing/index.d.ts +2 -1
- package/dist/sharing/index.d.ts.map +1 -1
- package/dist/sharing/index.js +2 -1
- package/dist/sharing/index.js.map +1 -1
- package/dist/ui/alert-dialog.d.ts +2 -0
- package/dist/ui/alert-dialog.d.ts.map +1 -1
- package/dist/ui/alert-dialog.js +1 -1
- package/dist/ui/alert-dialog.js.map +1 -1
- package/dist/ui/tooltip.d.ts +6 -1
- package/dist/ui/tooltip.d.ts.map +1 -1
- package/dist/ui/tooltip.js +13 -1
- package/dist/ui/tooltip.js.map +1 -1
- package/package.json +4 -4
- package/src/app/i18n/catalogs/ar-SA.ts +26 -0
- package/src/app/i18n/catalogs/de-DE.ts +25 -0
- package/src/app/i18n/catalogs/en-US.ts +25 -0
- package/src/app/i18n/catalogs/es-ES.ts +25 -0
- package/src/app/i18n/catalogs/fr-FR.ts +25 -0
- package/src/app/i18n/catalogs/hi-IN.ts +25 -0
- package/src/app/i18n/catalogs/ja-JP.ts +25 -0
- package/src/app/i18n/catalogs/ko-KR.ts +25 -0
- package/src/app/i18n/catalogs/pt-BR.ts +25 -0
- package/src/app/i18n/catalogs/zh-CN.ts +25 -0
- package/src/app/i18n/catalogs/zh-TW.ts +25 -0
- package/src/app/sharing/OwnershipFilter.tsx +35 -0
- package/src/app/sharing/ShareButton.tsx +384 -259
- package/src/app/sharing/ShareDialog.spec.tsx +50 -0
- package/src/app/sharing/ShareDialog.tsx +51 -15
- package/src/app/sharing/ShareVisibilityConfirmDialog.spec.tsx +138 -0
- package/src/app/sharing/ShareVisibilityConfirmDialog.tsx +124 -0
- package/src/app/sharing/ShareVisibilityKeyboard.spec.tsx +244 -0
- package/src/app/sharing/VisibilityBadge.spec.tsx +65 -0
- package/src/app/sharing/VisibilityBadge.tsx +31 -3
- package/src/app/sharing/index.ts +2 -0
- package/src/app/sharing/share-visibility-mutation-lifecycle.spec.tsx +1107 -0
- package/src/app/sharing/useShareButtonController.spec.tsx +226 -37
- package/src/app/sharing/useShareDialogController.spec.tsx +357 -9
- package/src/chat-history/ChatHistoryList.spec.tsx +157 -0
- package/src/chat-history/ChatHistoryList.tsx +104 -10
- package/src/chat-history/ChatHistoryRail.spec.tsx +50 -1
- package/src/chat-history/ChatHistoryRail.tsx +177 -26
- package/src/chat-history/ChatHistoryRailSections.spec.tsx +410 -0
- package/src/chat-history/index.ts +8 -0
- package/src/chat-history/useChatHistoryRailController.ts +94 -0
- package/src/chat-history/useChatHistoryRailSectionsController.spec.tsx +133 -0
- package/src/chat-history.css +29 -0
- package/src/composer/TiptapComposer.spec.ts +19 -0
- package/src/composer/TiptapComposer.tsx +26 -5
- package/src/sharing/OwnershipFilter.spec.tsx +108 -0
- package/src/sharing/OwnershipFilter.tsx +103 -0
- package/src/sharing/VisibilityBadge.spec.ts +53 -35
- package/src/sharing/VisibilityBadge.tsx +23 -3
- package/src/sharing/index.ts +11 -0
- package/src/ui/alert-dialog.tsx +7 -2
- package/src/ui/tooltip.tsx +22 -1
|
@@ -0,0 +1,410 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { act } from "react";
|
|
4
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
|
|
7
|
+
import type { ChatHistoryItem } from "./ChatHistoryList.js";
|
|
8
|
+
import {
|
|
9
|
+
ChatHistoryRail,
|
|
10
|
+
DefaultChatHistoryRailView,
|
|
11
|
+
type ChatHistoryRailSection,
|
|
12
|
+
} from "./ChatHistoryRail.js";
|
|
13
|
+
|
|
14
|
+
const railLabels = {
|
|
15
|
+
newChat: "New chat",
|
|
16
|
+
showMore: "Show more chats",
|
|
17
|
+
showLess: "Show fewer chats",
|
|
18
|
+
sections: {
|
|
19
|
+
showAll: "Show all shared",
|
|
20
|
+
showLess: "Show fewer shared",
|
|
21
|
+
loadMore: "Load more shared",
|
|
22
|
+
loadingMore: "Loading shared",
|
|
23
|
+
retry: "Retry shared",
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function makeItems(prefix: string, count: number): ChatHistoryItem[] {
|
|
28
|
+
return Array.from({ length: count }, (_, index) => ({
|
|
29
|
+
id: `${prefix}-${index + 1}`,
|
|
30
|
+
title: `${prefix} ${index + 1}`,
|
|
31
|
+
titleText: `${prefix} ${index + 1}`,
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("ChatHistoryRail sections", () => {
|
|
36
|
+
let container: HTMLDivElement;
|
|
37
|
+
let root: Root;
|
|
38
|
+
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
41
|
+
container = document.createElement("div");
|
|
42
|
+
document.body.appendChild(container);
|
|
43
|
+
root = createRoot(container);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
afterEach(() => {
|
|
47
|
+
act(() => root.unmount());
|
|
48
|
+
container.remove();
|
|
49
|
+
vi.restoreAllMocks();
|
|
50
|
+
vi.unstubAllGlobals();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
function renderRail(
|
|
54
|
+
sections: ChatHistoryRailSection[],
|
|
55
|
+
extra: Record<string, unknown> = {},
|
|
56
|
+
) {
|
|
57
|
+
act(() => {
|
|
58
|
+
root.render(
|
|
59
|
+
<ChatHistoryRail
|
|
60
|
+
sections={sections}
|
|
61
|
+
onSelect={() => {}}
|
|
62
|
+
onNewChat={() => {}}
|
|
63
|
+
railLabels={railLabels}
|
|
64
|
+
{...extra}
|
|
65
|
+
/>,
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
const section = (id: string) =>
|
|
70
|
+
container.querySelector<HTMLElement>(`[data-section-id="${id}"]`)!;
|
|
71
|
+
const rows = (id: string) =>
|
|
72
|
+
section(id).querySelectorAll(".an-chat-history-row");
|
|
73
|
+
const buttonByText = (scope: ParentNode, text: string) =>
|
|
74
|
+
Array.from(scope.querySelectorAll<HTMLButtonElement>("button")).find(
|
|
75
|
+
(button) => button.textContent?.includes(text),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
it("previews five shared rows, shows all of them, then collapses again", () => {
|
|
79
|
+
renderRail([
|
|
80
|
+
{ id: "mine", label: "My chats", items: makeItems("mine", 3) },
|
|
81
|
+
{
|
|
82
|
+
id: "shared",
|
|
83
|
+
label: "Shared with you",
|
|
84
|
+
items: makeItems("shared", 22),
|
|
85
|
+
},
|
|
86
|
+
]);
|
|
87
|
+
|
|
88
|
+
expect(rows("mine")).toHaveLength(3);
|
|
89
|
+
expect(rows("shared")).toHaveLength(5);
|
|
90
|
+
expect(buttonByText(section("mine"), "Show all shared")).toBeUndefined();
|
|
91
|
+
|
|
92
|
+
const toggle = buttonByText(section("shared"), "Show all shared")!;
|
|
93
|
+
expect(toggle.getAttribute("aria-expanded")).toBe("false");
|
|
94
|
+
act(() => toggle.click());
|
|
95
|
+
expect(rows("shared")).toHaveLength(22);
|
|
96
|
+
expect(toggle.getAttribute("aria-expanded")).toBe("true");
|
|
97
|
+
expect(toggle.textContent).toBe("Show fewer shared");
|
|
98
|
+
expect(rows("mine")).toHaveLength(3);
|
|
99
|
+
|
|
100
|
+
act(() => toggle.click());
|
|
101
|
+
expect(rows("shared")).toHaveLength(5);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("expands one section without touching another long section", () => {
|
|
105
|
+
renderRail([
|
|
106
|
+
{ id: "mine", items: makeItems("mine", 9) },
|
|
107
|
+
{ id: "shared", items: makeItems("shared", 9) },
|
|
108
|
+
]);
|
|
109
|
+
act(() => buttonByText(section("shared"), "Show all shared")?.click());
|
|
110
|
+
expect(rows("shared")).toHaveLength(9);
|
|
111
|
+
expect(rows("mine")).toHaveLength(5);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("pages the server: load-more appears once disclosed and is not capped at fifteen", () => {
|
|
115
|
+
const onLoadMore = vi.fn();
|
|
116
|
+
const render = (count: number, hasMore: boolean) =>
|
|
117
|
+
renderRail([
|
|
118
|
+
{
|
|
119
|
+
id: "shared",
|
|
120
|
+
items: makeItems("shared", count),
|
|
121
|
+
hasMore,
|
|
122
|
+
onLoadMore,
|
|
123
|
+
},
|
|
124
|
+
]);
|
|
125
|
+
render(20, true);
|
|
126
|
+
|
|
127
|
+
expect(buttonByText(section("shared"), "Load more shared")).toBeUndefined();
|
|
128
|
+
act(() => buttonByText(section("shared"), "Show all shared")?.click());
|
|
129
|
+
expect(rows("shared")).toHaveLength(20);
|
|
130
|
+
|
|
131
|
+
act(() => buttonByText(section("shared"), "Load more shared")?.click());
|
|
132
|
+
expect(onLoadMore).toHaveBeenCalledOnce();
|
|
133
|
+
|
|
134
|
+
render(40, true);
|
|
135
|
+
expect(rows("shared")).toHaveLength(40);
|
|
136
|
+
|
|
137
|
+
render(40, false);
|
|
138
|
+
expect(buttonByText(section("shared"), "Load more shared")).toBeUndefined();
|
|
139
|
+
expect(rows("shared")).toHaveLength(40);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("offers Show all for a short first page when the server has more", () => {
|
|
143
|
+
const onLoadMore = vi.fn();
|
|
144
|
+
renderRail([
|
|
145
|
+
{
|
|
146
|
+
id: "shared",
|
|
147
|
+
items: makeItems("shared", 2),
|
|
148
|
+
hasMore: true,
|
|
149
|
+
onLoadMore,
|
|
150
|
+
},
|
|
151
|
+
]);
|
|
152
|
+
act(() => buttonByText(section("shared"), "Show all shared")?.click());
|
|
153
|
+
act(() => buttonByText(section("shared"), "Load more shared")?.click());
|
|
154
|
+
expect(onLoadMore).toHaveBeenCalledOnce();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("shows a pending load-more that cannot be fired twice", () => {
|
|
158
|
+
const onLoadMore = vi.fn();
|
|
159
|
+
renderRail([
|
|
160
|
+
{
|
|
161
|
+
id: "shared",
|
|
162
|
+
items: makeItems("shared", 8),
|
|
163
|
+
hasMore: true,
|
|
164
|
+
loadingMore: true,
|
|
165
|
+
onLoadMore,
|
|
166
|
+
},
|
|
167
|
+
]);
|
|
168
|
+
act(() => buttonByText(section("shared"), "Show all shared")?.click());
|
|
169
|
+
const pending = buttonByText(section("shared"), "Loading shared")!;
|
|
170
|
+
expect(pending.disabled).toBe(true);
|
|
171
|
+
act(() => pending.click());
|
|
172
|
+
expect(onLoadMore).not.toHaveBeenCalled();
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("lets one section load, fail, or sit empty without touching the others", () => {
|
|
176
|
+
const onRetry = vi.fn();
|
|
177
|
+
renderRail([
|
|
178
|
+
{ id: "mine", items: makeItems("mine", 2) },
|
|
179
|
+
{ id: "shared", items: [], loading: true },
|
|
180
|
+
{ id: "org", items: [], error: "Load failed", onRetry },
|
|
181
|
+
{ id: "empty", items: [], emptyLabel: "Nothing here" },
|
|
182
|
+
]);
|
|
183
|
+
|
|
184
|
+
expect(rows("mine")).toHaveLength(2);
|
|
185
|
+
expect(
|
|
186
|
+
section("shared").querySelector('[aria-busy="true"]'),
|
|
187
|
+
).not.toBeNull();
|
|
188
|
+
expect(section("shared").textContent).not.toContain("Nothing");
|
|
189
|
+
expect(section("org").textContent).toContain("Load failed");
|
|
190
|
+
expect(section("empty").textContent).toBe("Nothing here");
|
|
191
|
+
|
|
192
|
+
act(() => buttonByText(section("org"), "Retry shared")?.click());
|
|
193
|
+
expect(onRetry).toHaveBeenCalledOnce();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("keeps already-loaded rows visible when a later load fails", () => {
|
|
197
|
+
renderRail([
|
|
198
|
+
{
|
|
199
|
+
id: "shared",
|
|
200
|
+
items: makeItems("shared", 3),
|
|
201
|
+
error: "Load more failed",
|
|
202
|
+
},
|
|
203
|
+
]);
|
|
204
|
+
expect(rows("shared")).toHaveLength(3);
|
|
205
|
+
expect(
|
|
206
|
+
section("shared").querySelector('[role="alert"]')?.textContent,
|
|
207
|
+
).toContain("Load more failed");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it("does not show the global empty state while a section has a state of its own", () => {
|
|
211
|
+
renderRail([{ id: "shared", items: [], loading: true }], {
|
|
212
|
+
emptyLabel: "No chats yet",
|
|
213
|
+
});
|
|
214
|
+
expect(container.textContent).not.toContain("No chats yet");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("shows the global empty state when every section is empty and has no state", () => {
|
|
218
|
+
renderRail(
|
|
219
|
+
[
|
|
220
|
+
{ id: "mine", label: "My chats", items: [] },
|
|
221
|
+
{ id: "shared", label: "Shared with you", items: [] },
|
|
222
|
+
],
|
|
223
|
+
{ emptyLabel: "No chats yet" },
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
expect(
|
|
227
|
+
container.querySelector(".an-chat-history__state")?.textContent,
|
|
228
|
+
).toBe("No chats yet");
|
|
229
|
+
expect(
|
|
230
|
+
container.querySelectorAll(".an-chat-history__section-label"),
|
|
231
|
+
).toHaveLength(0);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("shows a section empty message instead of the global empty state", () => {
|
|
235
|
+
renderRail(
|
|
236
|
+
[
|
|
237
|
+
{
|
|
238
|
+
id: "shared",
|
|
239
|
+
label: "Shared",
|
|
240
|
+
items: [],
|
|
241
|
+
emptyLabel: "No shared chats",
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
{
|
|
245
|
+
emptyLabel: "No chats yet",
|
|
246
|
+
},
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
expect(container.textContent).toContain("No shared chats");
|
|
250
|
+
expect(container.textContent).not.toContain("No chats yet");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("keeps zero-row server paging controls reachable", () => {
|
|
254
|
+
const onLoadMore = vi.fn();
|
|
255
|
+
renderRail([
|
|
256
|
+
{
|
|
257
|
+
id: "mine",
|
|
258
|
+
label: "My chats",
|
|
259
|
+
items: [],
|
|
260
|
+
hasMore: true,
|
|
261
|
+
onLoadMore,
|
|
262
|
+
},
|
|
263
|
+
]);
|
|
264
|
+
|
|
265
|
+
const mineSection = section("mine");
|
|
266
|
+
const showAll = buttonByText(mineSection, "Show all shared")!;
|
|
267
|
+
expect(showAll).toBeDefined();
|
|
268
|
+
act(() => showAll.click());
|
|
269
|
+
|
|
270
|
+
const loadMore = buttonByText(mineSection, "Load more shared")!;
|
|
271
|
+
expect(loadMore).toBeDefined();
|
|
272
|
+
act(() => loadMore.click());
|
|
273
|
+
expect(onLoadMore).toHaveBeenCalledOnce();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it("renders owner and access basis on a shared row", () => {
|
|
277
|
+
renderRail([
|
|
278
|
+
{
|
|
279
|
+
id: "shared",
|
|
280
|
+
items: [
|
|
281
|
+
{
|
|
282
|
+
id: "s1",
|
|
283
|
+
title: "Roadmap",
|
|
284
|
+
ownerLabel: "Pat Lee",
|
|
285
|
+
accessLabel: "Shared with your organization",
|
|
286
|
+
accessBasis: "organization",
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
},
|
|
290
|
+
]);
|
|
291
|
+
const access = container.querySelector(".an-chat-history-row__access");
|
|
292
|
+
expect(access?.getAttribute("data-access-basis")).toBe("organization");
|
|
293
|
+
expect(access?.textContent).toBe("Pat LeeShared with your organization");
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it("selecting a section row and New chat work as in the flat rail", () => {
|
|
297
|
+
const onSelect = vi.fn();
|
|
298
|
+
const onNewChat = vi.fn();
|
|
299
|
+
const onRename = vi.fn();
|
|
300
|
+
renderRail([{ id: "mine", items: makeItems("mine", 2) }], {
|
|
301
|
+
onSelect,
|
|
302
|
+
onNewChat,
|
|
303
|
+
onRename,
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
act(() =>
|
|
307
|
+
container
|
|
308
|
+
.querySelectorAll<HTMLButtonElement>(".an-chat-history-row__button")[1]
|
|
309
|
+
?.click(),
|
|
310
|
+
);
|
|
311
|
+
expect(onSelect).toHaveBeenCalledWith("mine-2");
|
|
312
|
+
|
|
313
|
+
act(() =>
|
|
314
|
+
container
|
|
315
|
+
.querySelector<HTMLButtonElement>(".an-chat-history-rail__new-chat")
|
|
316
|
+
?.click(),
|
|
317
|
+
);
|
|
318
|
+
expect(onNewChat).toHaveBeenCalledOnce();
|
|
319
|
+
expect(onRename).not.toHaveBeenCalled();
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it("renders sections and disclosures without a New chat button when showNewChat is false", () => {
|
|
323
|
+
const onNewChat = vi.fn();
|
|
324
|
+
act(() => {
|
|
325
|
+
root.render(
|
|
326
|
+
<ChatHistoryRail
|
|
327
|
+
sections={[
|
|
328
|
+
{ id: "mine", label: "My chats", items: makeItems("mine", 2) },
|
|
329
|
+
{ id: "shared", label: "Shared", items: makeItems("shared", 8) },
|
|
330
|
+
]}
|
|
331
|
+
onSelect={() => {}}
|
|
332
|
+
onNewChat={onNewChat}
|
|
333
|
+
showNewChat={false}
|
|
334
|
+
railLabels={railLabels}
|
|
335
|
+
renderRail={({ controller, ...context }) => (
|
|
336
|
+
<>
|
|
337
|
+
<button
|
|
338
|
+
type="button"
|
|
339
|
+
data-custom-new-chat
|
|
340
|
+
onClick={() => controller.onNewChat()}
|
|
341
|
+
/>
|
|
342
|
+
<DefaultChatHistoryRailView
|
|
343
|
+
controller={controller}
|
|
344
|
+
{...context}
|
|
345
|
+
/>
|
|
346
|
+
</>
|
|
347
|
+
)}
|
|
348
|
+
/>,
|
|
349
|
+
);
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
expect(rows("mine")).toHaveLength(2);
|
|
353
|
+
expect(rows("shared")).toHaveLength(5);
|
|
354
|
+
expect(
|
|
355
|
+
container.querySelector(".an-chat-history-rail__new-chat"),
|
|
356
|
+
).toBeNull();
|
|
357
|
+
expect(container.querySelector(".an-chat-history-rail__footer")).toBeNull();
|
|
358
|
+
act(() => buttonByText(section("shared"), "Show all shared")?.click());
|
|
359
|
+
expect(rows("shared")).toHaveLength(8);
|
|
360
|
+
|
|
361
|
+
act(() =>
|
|
362
|
+
container
|
|
363
|
+
.querySelector<HTMLButtonElement>("[data-custom-new-chat]")
|
|
364
|
+
?.click(),
|
|
365
|
+
);
|
|
366
|
+
expect(onNewChat).toHaveBeenCalledOnce();
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("shows the New chat button on a sectioned rail by default", () => {
|
|
370
|
+
renderRail([{ id: "mine", items: makeItems("mine", 2) }]);
|
|
371
|
+
expect(
|
|
372
|
+
container.querySelector(".an-chat-history-rail__new-chat"),
|
|
373
|
+
).not.toBeNull();
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it("keeps the flat items rail on its own five/fifteen cap", () => {
|
|
377
|
+
act(() => {
|
|
378
|
+
root.render(
|
|
379
|
+
<ChatHistoryRail
|
|
380
|
+
items={makeItems("flat", 30)}
|
|
381
|
+
onSelect={() => {}}
|
|
382
|
+
onNewChat={() => {}}
|
|
383
|
+
railLabels={{
|
|
384
|
+
newChat: "New chat",
|
|
385
|
+
showMore: "Show more chats",
|
|
386
|
+
showLess: "Show fewer chats",
|
|
387
|
+
}}
|
|
388
|
+
/>,
|
|
389
|
+
);
|
|
390
|
+
});
|
|
391
|
+
expect(container.querySelectorAll(".an-chat-history-row")).toHaveLength(5);
|
|
392
|
+
act(() =>
|
|
393
|
+
container
|
|
394
|
+
.querySelector<HTMLButtonElement>(".an-chat-history-rail__disclosure")
|
|
395
|
+
?.click(),
|
|
396
|
+
);
|
|
397
|
+
expect(container.querySelectorAll(".an-chat-history-row")).toHaveLength(15);
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it("hands custom renderers the section controllers", () => {
|
|
401
|
+
renderRail([{ id: "shared", items: makeItems("shared", 9) }], {
|
|
402
|
+
renderRail: ({
|
|
403
|
+
sections,
|
|
404
|
+
}: {
|
|
405
|
+
sections: { controller: { visibleItems: unknown[] } }[];
|
|
406
|
+
}) => <output>{sections[0]?.controller.visibleItems.length}</output>,
|
|
407
|
+
});
|
|
408
|
+
expect(container.querySelector("output")?.textContent).toBe("5");
|
|
409
|
+
});
|
|
410
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export {
|
|
2
2
|
ChatHistoryList,
|
|
3
|
+
type ChatHistoryAccessBasis,
|
|
3
4
|
type ChatHistoryItem,
|
|
4
5
|
type ChatHistoryListLabels,
|
|
5
6
|
type ChatHistoryListProps,
|
|
@@ -12,10 +13,17 @@ export {
|
|
|
12
13
|
type ChatHistoryRailLabels,
|
|
13
14
|
type ChatHistoryRailProps,
|
|
14
15
|
type ChatHistoryRailRenderContext,
|
|
16
|
+
type ChatHistoryRailSection,
|
|
17
|
+
type ChatHistoryRailSectionRenderState,
|
|
15
18
|
} from "./ChatHistoryRail.js";
|
|
16
19
|
export {
|
|
17
20
|
type ChatHistoryRailController,
|
|
18
21
|
type ChatHistoryRailControllerLabels,
|
|
22
|
+
type ChatHistoryRailSectionController,
|
|
23
|
+
type ChatHistoryRailSectionControllerLabels,
|
|
24
|
+
type ChatHistoryRailSectionInput,
|
|
25
|
+
DEFAULT_SECTION_PREVIEW_COUNT,
|
|
19
26
|
type UseChatHistoryRailControllerOptions,
|
|
20
27
|
useChatHistoryRailController,
|
|
28
|
+
useChatHistoryRailSectionsController,
|
|
21
29
|
} from "./useChatHistoryRailController.js";
|
|
@@ -74,3 +74,97 @@ export function useChatHistoryRailController<Item>({
|
|
|
74
74
|
resetExpanded,
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
|
+
|
|
78
|
+
export interface ChatHistoryRailSectionControllerLabels {
|
|
79
|
+
showAll: string;
|
|
80
|
+
showLess: string;
|
|
81
|
+
loadMore: string;
|
|
82
|
+
loadingMore: string;
|
|
83
|
+
retry: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface ChatHistoryRailSectionInput<Item> {
|
|
87
|
+
id: string;
|
|
88
|
+
items: readonly Item[];
|
|
89
|
+
/** Rows shown before disclosure. Defaults to 5. */
|
|
90
|
+
previewCount?: number;
|
|
91
|
+
/**
|
|
92
|
+
* The server has more rows than `items`. Disclosed sections then offer
|
|
93
|
+
* `onLoadMore`; disclosed rows are never capped, so a loaded page is never
|
|
94
|
+
* hidden. Ignored without `onLoadMore`.
|
|
95
|
+
*/
|
|
96
|
+
hasMore?: boolean;
|
|
97
|
+
loadingMore?: boolean;
|
|
98
|
+
onLoadMore?: () => void;
|
|
99
|
+
/** Overrides the rail-level section labels for this section. */
|
|
100
|
+
labels?: Partial<ChatHistoryRailSectionControllerLabels>;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface ChatHistoryRailSectionController<Item> {
|
|
104
|
+
id: string;
|
|
105
|
+
expanded: boolean;
|
|
106
|
+
previewLimit: number;
|
|
107
|
+
visibleItems: Item[];
|
|
108
|
+
/** More rows exist, loaded or on the server, than the preview shows. */
|
|
109
|
+
canExpand: boolean;
|
|
110
|
+
hasMore: boolean;
|
|
111
|
+
loadingMore: boolean;
|
|
112
|
+
/** Present only when the section is disclosed and the server has more. */
|
|
113
|
+
loadMore: (() => void) | null;
|
|
114
|
+
toggleExpanded: () => void;
|
|
115
|
+
labels: ChatHistoryRailSectionControllerLabels;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export const DEFAULT_SECTION_PREVIEW_COUNT = 5;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Independent preview/disclosure per section, keyed by section id. Disclosure
|
|
122
|
+
* state is kept here rather than in each section so a section whose rows are
|
|
123
|
+
* replaced by a refetch keeps its state.
|
|
124
|
+
*/
|
|
125
|
+
export function useChatHistoryRailSectionsController<Item>(
|
|
126
|
+
sections: readonly ChatHistoryRailSectionInput<Item>[],
|
|
127
|
+
sectionLabels: ChatHistoryRailSectionControllerLabels | undefined,
|
|
128
|
+
): ChatHistoryRailSectionController<Item>[] {
|
|
129
|
+
const [expandedIds, setExpandedIds] = useState<ReadonlySet<string>>(
|
|
130
|
+
() => new Set(),
|
|
131
|
+
);
|
|
132
|
+
const toggle = useCallback((id: string) => {
|
|
133
|
+
setExpandedIds((current) => {
|
|
134
|
+
const next = new Set(current);
|
|
135
|
+
if (next.has(id)) next.delete(id);
|
|
136
|
+
else next.add(id);
|
|
137
|
+
return next;
|
|
138
|
+
});
|
|
139
|
+
}, []);
|
|
140
|
+
|
|
141
|
+
return sections.map((section) => {
|
|
142
|
+
const previewLimit = normalizeLimit(
|
|
143
|
+
section.previewCount,
|
|
144
|
+
DEFAULT_SECTION_PREVIEW_COUNT,
|
|
145
|
+
);
|
|
146
|
+
const onLoadMore = section.onLoadMore;
|
|
147
|
+
const hasMore = Boolean(section.hasMore && onLoadMore);
|
|
148
|
+
const canExpand = section.items.length > previewLimit || hasMore;
|
|
149
|
+
const expanded = canExpand && expandedIds.has(section.id);
|
|
150
|
+
return {
|
|
151
|
+
id: section.id,
|
|
152
|
+
expanded,
|
|
153
|
+
previewLimit,
|
|
154
|
+
visibleItems: expanded
|
|
155
|
+
? [...section.items]
|
|
156
|
+
: section.items.slice(0, previewLimit),
|
|
157
|
+
canExpand,
|
|
158
|
+
hasMore,
|
|
159
|
+
loadingMore: Boolean(section.loadingMore),
|
|
160
|
+
loadMore: expanded && hasMore && onLoadMore ? onLoadMore : null,
|
|
161
|
+
toggleExpanded: () => {
|
|
162
|
+
if (canExpand) toggle(section.id);
|
|
163
|
+
},
|
|
164
|
+
labels: {
|
|
165
|
+
...sectionLabels,
|
|
166
|
+
...section.labels,
|
|
167
|
+
} as ChatHistoryRailSectionControllerLabels,
|
|
168
|
+
};
|
|
169
|
+
});
|
|
170
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
|
|
3
|
+
import { act } from "react";
|
|
4
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
type ChatHistoryRailSectionController,
|
|
9
|
+
type ChatHistoryRailSectionControllerLabels,
|
|
10
|
+
type ChatHistoryRailSectionInput,
|
|
11
|
+
useChatHistoryRailSectionsController,
|
|
12
|
+
} from "./useChatHistoryRailController.js";
|
|
13
|
+
|
|
14
|
+
interface Item {
|
|
15
|
+
id: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const labels: ChatHistoryRailSectionControllerLabels = {
|
|
19
|
+
showAll: "Show all",
|
|
20
|
+
showLess: "Show less",
|
|
21
|
+
loadMore: "Load more",
|
|
22
|
+
loadingMore: "Loading more",
|
|
23
|
+
retry: "Retry",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function items(count: number): Item[] {
|
|
27
|
+
return Array.from({ length: count }, (_, index) => ({ id: `c${index}` }));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("useChatHistoryRailSectionsController", () => {
|
|
31
|
+
let container: HTMLDivElement;
|
|
32
|
+
let root: Root;
|
|
33
|
+
let controllers: ChatHistoryRailSectionController<Item>[];
|
|
34
|
+
|
|
35
|
+
function Harness({
|
|
36
|
+
sections,
|
|
37
|
+
}: {
|
|
38
|
+
sections: ChatHistoryRailSectionInput<Item>[];
|
|
39
|
+
}) {
|
|
40
|
+
controllers = useChatHistoryRailSectionsController(sections, labels);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const render = (sections: ChatHistoryRailSectionInput<Item>[]) =>
|
|
44
|
+
act(() => root.render(<Harness sections={sections} />));
|
|
45
|
+
const byId = (id: string) => controllers.find((c) => c.id === id)!;
|
|
46
|
+
|
|
47
|
+
beforeEach(() => {
|
|
48
|
+
vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
|
|
49
|
+
container = document.createElement("div");
|
|
50
|
+
document.body.appendChild(container);
|
|
51
|
+
root = createRoot(container);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
afterEach(() => {
|
|
55
|
+
act(() => root.unmount());
|
|
56
|
+
container.remove();
|
|
57
|
+
vi.unstubAllGlobals();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("previews five rows by default and discloses every loaded row, not fifteen", () => {
|
|
61
|
+
render([{ id: "shared", items: items(40) }]);
|
|
62
|
+
expect(byId("shared").visibleItems).toHaveLength(5);
|
|
63
|
+
expect(byId("shared").canExpand).toBe(true);
|
|
64
|
+
|
|
65
|
+
act(() => byId("shared").toggleExpanded());
|
|
66
|
+
expect(byId("shared").visibleItems).toHaveLength(40);
|
|
67
|
+
|
|
68
|
+
act(() => byId("shared").toggleExpanded());
|
|
69
|
+
expect(byId("shared").visibleItems).toHaveLength(5);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("keeps each section's disclosure independent", () => {
|
|
73
|
+
render([
|
|
74
|
+
{ id: "mine", items: items(8) },
|
|
75
|
+
{ id: "shared", items: items(8) },
|
|
76
|
+
]);
|
|
77
|
+
act(() => byId("shared").toggleExpanded());
|
|
78
|
+
expect(byId("shared").expanded).toBe(true);
|
|
79
|
+
expect(byId("mine").expanded).toBe(false);
|
|
80
|
+
expect(byId("mine").visibleItems).toHaveLength(5);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("does not offer disclosure for a short section the server has nothing more for", () => {
|
|
84
|
+
render([{ id: "shared", items: items(5) }]);
|
|
85
|
+
expect(byId("shared").canExpand).toBe(false);
|
|
86
|
+
act(() => byId("shared").toggleExpanded());
|
|
87
|
+
expect(byId("shared").expanded).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("offers disclosure and load-more for a short section with more on the server", () => {
|
|
91
|
+
const onLoadMore = vi.fn();
|
|
92
|
+
render([{ id: "shared", items: items(3), hasMore: true, onLoadMore }]);
|
|
93
|
+
expect(byId("shared").canExpand).toBe(true);
|
|
94
|
+
expect(byId("shared").loadMore).toBeNull();
|
|
95
|
+
|
|
96
|
+
act(() => byId("shared").toggleExpanded());
|
|
97
|
+
byId("shared").loadMore?.();
|
|
98
|
+
expect(onLoadMore).toHaveBeenCalledOnce();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("treats hasMore without a loader as nothing more to show", () => {
|
|
102
|
+
render([{ id: "shared", items: items(3), hasMore: true }]);
|
|
103
|
+
expect(byId("shared").hasMore).toBe(false);
|
|
104
|
+
expect(byId("shared").canExpand).toBe(false);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("keeps a section disclosed while its rows are replaced by a page load", () => {
|
|
108
|
+
const onLoadMore = vi.fn();
|
|
109
|
+
render([{ id: "shared", items: items(6), hasMore: true, onLoadMore }]);
|
|
110
|
+
act(() => byId("shared").toggleExpanded());
|
|
111
|
+
render([{ id: "shared", items: items(12), hasMore: true, onLoadMore }]);
|
|
112
|
+
expect(byId("shared").expanded).toBe(true);
|
|
113
|
+
expect(byId("shared").visibleItems).toHaveLength(12);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("collapses when nothing is left to disclose", () => {
|
|
117
|
+
render([{ id: "shared", items: items(8) }]);
|
|
118
|
+
act(() => byId("shared").toggleExpanded());
|
|
119
|
+
render([{ id: "shared", items: items(2) }]);
|
|
120
|
+
expect(byId("shared").expanded).toBe(false);
|
|
121
|
+
expect(byId("shared").visibleItems).toHaveLength(2);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it("lets a section override the rail-level labels", () => {
|
|
125
|
+
render([
|
|
126
|
+
{ id: "a", items: items(1) },
|
|
127
|
+
{ id: "b", items: items(1), labels: { showAll: "Everything" } },
|
|
128
|
+
]);
|
|
129
|
+
expect(byId("a").labels.showAll).toBe("Show all");
|
|
130
|
+
expect(byId("b").labels.showAll).toBe("Everything");
|
|
131
|
+
expect(byId("b").labels.loadMore).toBe("Load more");
|
|
132
|
+
});
|
|
133
|
+
});
|
package/src/chat-history.css
CHANGED
|
@@ -57,6 +57,35 @@
|
|
|
57
57
|
color: hsl(var(--muted-foreground));
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
+
.an-chat-history__skeletons {
|
|
61
|
+
display: grid;
|
|
62
|
+
gap: 6px;
|
|
63
|
+
padding: 6px 12px;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.an-chat-history-rail__section-footer {
|
|
67
|
+
display: flex;
|
|
68
|
+
flex-wrap: wrap;
|
|
69
|
+
align-items: center;
|
|
70
|
+
gap: 2px;
|
|
71
|
+
padding: 0 4px;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.an-chat-history-row__access {
|
|
75
|
+
display: flex;
|
|
76
|
+
gap: 6px;
|
|
77
|
+
min-width: 0;
|
|
78
|
+
margin-top: 2px;
|
|
79
|
+
font-size: 10px;
|
|
80
|
+
color: hsl(var(--muted-foreground) / 0.7);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
.an-chat-history-row__owner {
|
|
84
|
+
overflow: hidden;
|
|
85
|
+
text-overflow: ellipsis;
|
|
86
|
+
white-space: nowrap;
|
|
87
|
+
}
|
|
88
|
+
|
|
60
89
|
.an-chat-history__state--error {
|
|
61
90
|
color: hsl(38 92% 50%);
|
|
62
91
|
}
|