@assistant-ui/react-native 0.1.45 → 0.1.47
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/index.d.ts +13 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/primitives/actionBar/ActionBarCopy.d.ts +2 -2
- package/dist/primitives/actionBar/ActionBarCopy.d.ts.map +1 -1
- package/dist/primitives/actionBar/ActionBarCopy.js +2 -1
- package/dist/primitives/actionBar/ActionBarCopy.js.map +1 -1
- package/dist/primitives/actionBar/ActionBarFeedbackNegative.d.ts +2 -2
- package/dist/primitives/actionBar/ActionBarFeedbackNegative.d.ts.map +1 -1
- package/dist/primitives/actionBar/ActionBarFeedbackNegative.js +2 -1
- package/dist/primitives/actionBar/ActionBarFeedbackNegative.js.map +1 -1
- package/dist/primitives/actionBar/ActionBarFeedbackPositive.d.ts +2 -2
- package/dist/primitives/actionBar/ActionBarFeedbackPositive.d.ts.map +1 -1
- package/dist/primitives/actionBar/ActionBarFeedbackPositive.js +2 -1
- package/dist/primitives/actionBar/ActionBarFeedbackPositive.js.map +1 -1
- package/dist/primitives/composer/ComposerInput.d.ts +1 -1
- package/dist/primitives/composer/ComposerInput.d.ts.map +1 -1
- package/dist/primitives/composer/ComposerInput.js +55 -38
- package/dist/primitives/composer/ComposerInput.js.map +1 -1
- package/dist/primitives/message/MessageContent.d.ts.map +1 -1
- package/dist/primitives/message/MessageContent.js +13 -10
- package/dist/primitives/message/MessageContent.js.map +1 -1
- package/dist/primitives/message/MessageParts.d.ts +1 -0
- package/dist/primitives/message/MessageParts.d.ts.map +1 -1
- package/dist/primitives/message/MessageParts.js +22 -3
- package/dist/primitives/message/MessageParts.js.map +1 -1
- package/dist/primitives/message.d.ts +1 -0
- package/dist/primitives/message.d.ts.map +1 -1
- package/dist/primitives/message.js +4 -3
- package/dist/primitives/message.js.map +1 -1
- package/dist/primitives/messagePart.js +1 -1
- package/dist/primitives/thread/ThreadMessages.d.ts +7 -5
- package/dist/primitives/thread/ThreadMessages.d.ts.map +1 -1
- package/dist/primitives/thread/ThreadMessages.js +109 -376
- package/dist/primitives/thread/ThreadMessages.js.map +1 -1
- package/dist/primitives/thread/ThreadRowsFlatList.d.ts +29 -0
- package/dist/primitives/thread/ThreadRowsFlatList.d.ts.map +1 -0
- package/dist/primitives/thread/ThreadRowsFlatList.js +226 -0
- package/dist/primitives/thread/ThreadRowsFlatList.js.map +1 -0
- package/dist/primitives/thread/flatListScroll.d.ts +52 -0
- package/dist/primitives/thread/flatListScroll.d.ts.map +1 -0
- package/dist/primitives/thread/flatListScroll.js +380 -0
- package/dist/primitives/thread/flatListScroll.js.map +1 -0
- package/dist/primitives/thread.d.ts +2 -0
- package/dist/primitives/thread.d.ts.map +1 -1
- package/dist/primitives/thread.js +5 -2
- package/dist/primitives/thread.js.map +1 -1
- package/dist/primitives/threadListItem/ThreadListItemTitle.d.ts +10 -0
- package/dist/primitives/threadListItem/ThreadListItemTitle.d.ts.map +1 -0
- package/dist/primitives/threadListItem/ThreadListItemTitle.js +47 -0
- package/dist/primitives/threadListItem/ThreadListItemTitle.js.map +1 -0
- package/dist/primitives/threadListItem.d.ts +1 -1
- package/dist/primitives/threadListItem.d.ts.map +1 -1
- package/dist/primitives/threadListItem.js +3 -3
- package/dist/primitives/threadListItem.js.map +1 -1
- package/package.json +8 -7
- package/src/index.ts +22 -1
- package/src/primitives/actionBar/ActionBar.test.tsx +45 -0
- package/src/primitives/actionBar/ActionBarCopy.tsx +12 -3
- package/src/primitives/actionBar/ActionBarFeedbackNegative.tsx +13 -3
- package/src/primitives/actionBar/ActionBarFeedbackPositive.tsx +13 -3
- package/src/primitives/composer/ComposerInput.test.tsx +64 -4
- package/src/primitives/composer/ComposerInput.tsx +13 -2
- package/src/primitives/message/MessageContent.runtime.test.tsx +35 -0
- package/src/primitives/message/MessageContent.test.tsx +68 -4
- package/src/primitives/message/MessageContent.tsx +19 -9
- package/src/primitives/message/MessageParts.test.tsx +68 -1
- package/src/primitives/message/MessageParts.tsx +17 -1
- package/src/primitives/message.test.tsx +74 -0
- package/src/primitives/message.ts +1 -0
- package/src/primitives/thread/ThreadMessages.test.tsx +28 -1
- package/src/primitives/thread/ThreadMessages.tsx +49 -343
- package/src/primitives/thread/ThreadRowsFlatList.test.tsx +253 -0
- package/src/primitives/thread/ThreadRowsFlatList.tsx +159 -0
- package/src/primitives/thread/flatListScroll.ts +467 -0
- package/src/primitives/thread.ts +5 -0
- package/src/primitives/threadListItem/ThreadListItemTitle.test.tsx +164 -0
- package/src/primitives/threadListItem/ThreadListItemTitle.tsx +23 -0
- package/src/primitives/threadListItem.ts +4 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { act } from "react";
|
|
2
2
|
import { createRoot, type Root } from "react-dom/client";
|
|
3
3
|
import { Text } from "react-native";
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
5
|
import type { ThreadMessageLike } from "@assistant-ui/core";
|
|
6
6
|
import {
|
|
7
7
|
AssistantRuntimeProvider,
|
|
@@ -10,6 +10,23 @@ import {
|
|
|
10
10
|
} from "@assistant-ui/core/react";
|
|
11
11
|
import { MessagePrimitiveParts } from "./MessageParts";
|
|
12
12
|
|
|
13
|
+
const h = vi.hoisted(() => ({
|
|
14
|
+
imageProps: null as Record<string, unknown> | null,
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock("react-native", async (importOriginal) => {
|
|
18
|
+
const actual = await importOriginal<typeof import("react-native")>();
|
|
19
|
+
const React = await import("react");
|
|
20
|
+
const ImageMock = (props: Record<string, unknown>) => {
|
|
21
|
+
h.imageProps = props;
|
|
22
|
+
return React.createElement(
|
|
23
|
+
actual.Image as unknown as React.ElementType,
|
|
24
|
+
props,
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
return { ...actual, Image: ImageMock };
|
|
28
|
+
});
|
|
29
|
+
|
|
13
30
|
(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
|
|
14
31
|
|
|
15
32
|
const messages: ThreadMessageLike[] = [
|
|
@@ -73,11 +90,33 @@ const App = (props: MessagePrimitiveParts.Props) => {
|
|
|
73
90
|
|
|
74
91
|
const ChainOfThought = () => <Text>Thought group</Text>;
|
|
75
92
|
|
|
93
|
+
const IMAGE = "data:image/png;base64,iVBORw0KGgo=";
|
|
94
|
+
|
|
95
|
+
const ImageApp = (props: MessagePrimitiveParts.Props) => {
|
|
96
|
+
const runtime = useExternalStoreRuntime({
|
|
97
|
+
messages: [
|
|
98
|
+
{ role: "assistant", content: [{ type: "image", image: IMAGE }] },
|
|
99
|
+
] satisfies ThreadMessageLike[],
|
|
100
|
+
convertMessage: (message) => message,
|
|
101
|
+
onNew: async () => {
|
|
102
|
+
throw new Error("This thread is read-only");
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
return (
|
|
106
|
+
<AssistantRuntimeProvider runtime={runtime}>
|
|
107
|
+
<MessageByIndexProvider index={0}>
|
|
108
|
+
<MessagePrimitiveParts {...props} />
|
|
109
|
+
</MessageByIndexProvider>
|
|
110
|
+
</AssistantRuntimeProvider>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
76
114
|
describe("MessagePrimitiveParts", () => {
|
|
77
115
|
let container: HTMLDivElement;
|
|
78
116
|
let root: Root;
|
|
79
117
|
|
|
80
118
|
beforeEach(() => {
|
|
119
|
+
h.imageProps = null;
|
|
81
120
|
container = document.createElement("div");
|
|
82
121
|
document.body.appendChild(container);
|
|
83
122
|
root = createRoot(container);
|
|
@@ -88,6 +127,34 @@ describe("MessagePrimitiveParts", () => {
|
|
|
88
127
|
container.remove();
|
|
89
128
|
});
|
|
90
129
|
|
|
130
|
+
it("renders an image part without a components override", async () => {
|
|
131
|
+
await act(async () => root.render(<ImageApp />));
|
|
132
|
+
|
|
133
|
+
const image = container.querySelector("img");
|
|
134
|
+
expect(image).not.toBeNull();
|
|
135
|
+
expect(image?.getAttribute("src")).toContain(IMAGE);
|
|
136
|
+
expect(h.imageProps?.source).toEqual({ uri: IMAGE });
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("gives the default image a sizing style, which native layout requires", async () => {
|
|
140
|
+
await act(async () => root.render(<ImageApp />));
|
|
141
|
+
|
|
142
|
+
expect(h.imageProps?.style).toEqual({ width: "100%", aspectRatio: 1 });
|
|
143
|
+
expect(h.imageProps?.resizeMode).toBe("contain");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("lets a caller-supplied Image win over the default", async () => {
|
|
147
|
+
const CustomImage = () => <Text>custom image</Text>;
|
|
148
|
+
|
|
149
|
+
await act(async () =>
|
|
150
|
+
root.render(<ImageApp components={{ Image: CustomImage }} />),
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
expect(container.textContent).toBe("custom image");
|
|
154
|
+
expect(container.querySelector("img")).toBeNull();
|
|
155
|
+
expect(h.imageProps).toBeNull();
|
|
156
|
+
});
|
|
157
|
+
|
|
91
158
|
it("renders generative UI and its fallback beside native text", async () => {
|
|
92
159
|
await act(async () => root.render(<App components={components} />));
|
|
93
160
|
expect(container.textContent).toBe(
|
|
@@ -6,10 +6,22 @@ import {
|
|
|
6
6
|
MessagePrimitivePartByIndex as MessagePrimitivePartByIndexBase,
|
|
7
7
|
messagePartsDefaultComponents,
|
|
8
8
|
} from "@assistant-ui/core/react";
|
|
9
|
+
import { MessagePartPrimitiveImage } from "../messagePart/MessagePartImage";
|
|
10
|
+
|
|
11
|
+
// React Native derives no intrinsic size from a remote or data URI, so an
|
|
12
|
+
// Image with no dimensions lays out at zero. The default fills the available
|
|
13
|
+
// width and keeps the picture whole inside a square box; a consumer that knows
|
|
14
|
+
// its own aspect ratio overrides `components.Image`.
|
|
15
|
+
const DEFAULT_IMAGE_STYLE = { width: "100%", aspectRatio: 1 } as const;
|
|
16
|
+
|
|
17
|
+
export const DefaultMessageImage = () => (
|
|
18
|
+
<MessagePartPrimitiveImage style={DEFAULT_IMAGE_STYLE} resizeMode="contain" />
|
|
19
|
+
);
|
|
9
20
|
|
|
10
21
|
const rnDefaultComponents = {
|
|
11
22
|
...messagePartsDefaultComponents,
|
|
12
23
|
Text: ({ text }: { text: string }) => <Text>{text}</Text>,
|
|
24
|
+
Image: DefaultMessageImage,
|
|
13
25
|
} satisfies MessagePrimitiveParts.Props["components"];
|
|
14
26
|
|
|
15
27
|
export namespace MessagePrimitiveParts {
|
|
@@ -30,7 +42,11 @@ export const MessagePrimitiveParts: FC<MessagePrimitiveParts.Props> = (
|
|
|
30
42
|
|
|
31
43
|
const { components, ...rest } = props;
|
|
32
44
|
const merged = components
|
|
33
|
-
? {
|
|
45
|
+
? {
|
|
46
|
+
...components,
|
|
47
|
+
Text: components.Text ?? rnDefaultComponents.Text,
|
|
48
|
+
Image: components.Image ?? rnDefaultComponents.Image,
|
|
49
|
+
}
|
|
34
50
|
: rnDefaultComponents;
|
|
35
51
|
|
|
36
52
|
return <MessagePrimitivePartsBase components={merged as any} {...rest} />;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { act, type ReactElement } from "react";
|
|
2
|
+
import { createRoot, type Root } from "react-dom/client";
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
4
|
+
import { Text } from "react-native";
|
|
5
|
+
import * as MessagePrimitive from "./message";
|
|
6
|
+
|
|
7
|
+
const h = vi.hoisted(() => ({
|
|
8
|
+
message: { metadata: { custom: {} } as { custom: Record<string, unknown> } },
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
vi.mock("@assistant-ui/store", async (importOriginal) => {
|
|
12
|
+
const actual = await importOriginal<typeof import("@assistant-ui/store")>();
|
|
13
|
+
return {
|
|
14
|
+
...actual,
|
|
15
|
+
useAuiState: <T,>(selector: (state: typeof h) => T) => selector(h),
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
|
|
20
|
+
|
|
21
|
+
describe("MessagePrimitive.Quote", () => {
|
|
22
|
+
let container: HTMLDivElement;
|
|
23
|
+
let root: Root;
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
h.message = { metadata: { custom: {} } };
|
|
27
|
+
container = document.createElement("div");
|
|
28
|
+
document.body.appendChild(container);
|
|
29
|
+
root = createRoot(container);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
afterEach(async () => {
|
|
33
|
+
await act(async () => {
|
|
34
|
+
root.unmount();
|
|
35
|
+
});
|
|
36
|
+
container.remove();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const render = async (element: ReactElement) => {
|
|
40
|
+
await act(async () => {
|
|
41
|
+
root.render(element);
|
|
42
|
+
});
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
it("renders nothing for a message without a quote", async () => {
|
|
46
|
+
await render(
|
|
47
|
+
<MessagePrimitive.Quote>
|
|
48
|
+
{({ text }) => <Text testID="quote">{text}</Text>}
|
|
49
|
+
</MessagePrimitive.Quote>,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(container.querySelector('[data-testid="quote"]')).toBeNull();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("passes the quote text and source message id to its child", async () => {
|
|
56
|
+
h.message = {
|
|
57
|
+
metadata: { custom: { quote: { text: "quoted text", messageId: "m1" } } },
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
await render(
|
|
61
|
+
<MessagePrimitive.Quote>
|
|
62
|
+
{({ text, messageId }) => (
|
|
63
|
+
<Text testID="quote">
|
|
64
|
+
{messageId}: {text}
|
|
65
|
+
</Text>
|
|
66
|
+
)}
|
|
67
|
+
</MessagePrimitive.Quote>,
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
expect(container.querySelector('[data-testid="quote"]')?.textContent).toBe(
|
|
71
|
+
"m1: quoted text",
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -11,6 +11,7 @@ export {
|
|
|
11
11
|
MessagePrimitivePartByIndex as PartByIndex,
|
|
12
12
|
} from "./message/MessageParts";
|
|
13
13
|
export { MessagePrimitiveGroupedParts as GroupedParts } from "@assistant-ui/core/react";
|
|
14
|
+
export { MessagePrimitiveQuote as Quote } from "@assistant-ui/core/react";
|
|
14
15
|
export {
|
|
15
16
|
MessageIf as If,
|
|
16
17
|
type MessageIfProps as IfProps,
|
|
@@ -10,13 +10,18 @@ type Msg = { id: string; role: string };
|
|
|
10
10
|
|
|
11
11
|
const h = vi.hoisted(() => ({
|
|
12
12
|
state: {
|
|
13
|
-
thread: {
|
|
13
|
+
thread: {
|
|
14
|
+
messages: [] as Msg[],
|
|
15
|
+
hasEarlier: false,
|
|
16
|
+
isLoadingEarlier: false,
|
|
17
|
+
},
|
|
14
18
|
message: { role: "user" as string, composer: { isEditing: false } },
|
|
15
19
|
},
|
|
16
20
|
itemState: { role: "user" } as { role: string; parts?: unknown[] },
|
|
17
21
|
events: {} as Record<string, Set<() => void>>,
|
|
18
22
|
flatListProps: null as Record<string, unknown> | null,
|
|
19
23
|
scrollToOffset: vi.fn(),
|
|
24
|
+
loadEarlier: vi.fn(async () => {}),
|
|
20
25
|
}));
|
|
21
26
|
|
|
22
27
|
vi.mock("react-native", async (importOriginal) => {
|
|
@@ -79,7 +84,9 @@ vi.mock("react-native", async (importOriginal) => {
|
|
|
79
84
|
vi.mock("@assistant-ui/store", async () => {
|
|
80
85
|
const React = await import("react");
|
|
81
86
|
|
|
87
|
+
const aui = { thread: { loadEarlier: h.loadEarlier } };
|
|
82
88
|
return {
|
|
89
|
+
useAui: () => aui,
|
|
83
90
|
useAuiState: <T,>(selector: (s: typeof h.state) => T) => selector(h.state),
|
|
84
91
|
useAuiEvent: (
|
|
85
92
|
selector: string | { scope: string; event: string },
|
|
@@ -400,6 +407,26 @@ describe("ThreadMessages", () => {
|
|
|
400
407
|
});
|
|
401
408
|
|
|
402
409
|
describe("MessagesFlatList history", () => {
|
|
410
|
+
it("pages through the runtime when no history prop is given", async () => {
|
|
411
|
+
h.state.thread.hasEarlier = true;
|
|
412
|
+
try {
|
|
413
|
+
await mountFlatList({ components: messageComponents });
|
|
414
|
+
getFlatListProps().onStartReached?.({ distanceFromStart: 0 });
|
|
415
|
+
expect(h.loadEarlier).toHaveBeenCalledOnce();
|
|
416
|
+
|
|
417
|
+
h.state.thread.isLoadingEarlier = true;
|
|
418
|
+
await mountFlatList({ components: messageComponents });
|
|
419
|
+
expect(getFlatListProps().onStartReached).toBeUndefined();
|
|
420
|
+
} finally {
|
|
421
|
+
h.state.thread.hasEarlier = false;
|
|
422
|
+
h.state.thread.isLoadingEarlier = false;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
await mountFlatList({ components: messageComponents });
|
|
426
|
+
expect(getFlatListProps().onStartReached).toBeUndefined();
|
|
427
|
+
expect(getFlatListProps().onStartReachedThreshold).toBeUndefined();
|
|
428
|
+
});
|
|
429
|
+
|
|
403
430
|
it("wires loadMore only while history can load more", async () => {
|
|
404
431
|
const loadMore = vi.fn();
|
|
405
432
|
|