@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.
Files changed (79) hide show
  1. package/dist/index.d.ts +13 -4
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +3 -3
  4. package/dist/primitives/actionBar/ActionBarCopy.d.ts +2 -2
  5. package/dist/primitives/actionBar/ActionBarCopy.d.ts.map +1 -1
  6. package/dist/primitives/actionBar/ActionBarCopy.js +2 -1
  7. package/dist/primitives/actionBar/ActionBarCopy.js.map +1 -1
  8. package/dist/primitives/actionBar/ActionBarFeedbackNegative.d.ts +2 -2
  9. package/dist/primitives/actionBar/ActionBarFeedbackNegative.d.ts.map +1 -1
  10. package/dist/primitives/actionBar/ActionBarFeedbackNegative.js +2 -1
  11. package/dist/primitives/actionBar/ActionBarFeedbackNegative.js.map +1 -1
  12. package/dist/primitives/actionBar/ActionBarFeedbackPositive.d.ts +2 -2
  13. package/dist/primitives/actionBar/ActionBarFeedbackPositive.d.ts.map +1 -1
  14. package/dist/primitives/actionBar/ActionBarFeedbackPositive.js +2 -1
  15. package/dist/primitives/actionBar/ActionBarFeedbackPositive.js.map +1 -1
  16. package/dist/primitives/composer/ComposerInput.d.ts +1 -1
  17. package/dist/primitives/composer/ComposerInput.d.ts.map +1 -1
  18. package/dist/primitives/composer/ComposerInput.js +55 -38
  19. package/dist/primitives/composer/ComposerInput.js.map +1 -1
  20. package/dist/primitives/message/MessageContent.d.ts.map +1 -1
  21. package/dist/primitives/message/MessageContent.js +13 -10
  22. package/dist/primitives/message/MessageContent.js.map +1 -1
  23. package/dist/primitives/message/MessageParts.d.ts +1 -0
  24. package/dist/primitives/message/MessageParts.d.ts.map +1 -1
  25. package/dist/primitives/message/MessageParts.js +22 -3
  26. package/dist/primitives/message/MessageParts.js.map +1 -1
  27. package/dist/primitives/message.d.ts +1 -0
  28. package/dist/primitives/message.d.ts.map +1 -1
  29. package/dist/primitives/message.js +4 -3
  30. package/dist/primitives/message.js.map +1 -1
  31. package/dist/primitives/messagePart.js +1 -1
  32. package/dist/primitives/thread/ThreadMessages.d.ts +7 -5
  33. package/dist/primitives/thread/ThreadMessages.d.ts.map +1 -1
  34. package/dist/primitives/thread/ThreadMessages.js +109 -376
  35. package/dist/primitives/thread/ThreadMessages.js.map +1 -1
  36. package/dist/primitives/thread/ThreadRowsFlatList.d.ts +29 -0
  37. package/dist/primitives/thread/ThreadRowsFlatList.d.ts.map +1 -0
  38. package/dist/primitives/thread/ThreadRowsFlatList.js +226 -0
  39. package/dist/primitives/thread/ThreadRowsFlatList.js.map +1 -0
  40. package/dist/primitives/thread/flatListScroll.d.ts +52 -0
  41. package/dist/primitives/thread/flatListScroll.d.ts.map +1 -0
  42. package/dist/primitives/thread/flatListScroll.js +380 -0
  43. package/dist/primitives/thread/flatListScroll.js.map +1 -0
  44. package/dist/primitives/thread.d.ts +2 -0
  45. package/dist/primitives/thread.d.ts.map +1 -1
  46. package/dist/primitives/thread.js +5 -2
  47. package/dist/primitives/thread.js.map +1 -1
  48. package/dist/primitives/threadListItem/ThreadListItemTitle.d.ts +10 -0
  49. package/dist/primitives/threadListItem/ThreadListItemTitle.d.ts.map +1 -0
  50. package/dist/primitives/threadListItem/ThreadListItemTitle.js +47 -0
  51. package/dist/primitives/threadListItem/ThreadListItemTitle.js.map +1 -0
  52. package/dist/primitives/threadListItem.d.ts +1 -1
  53. package/dist/primitives/threadListItem.d.ts.map +1 -1
  54. package/dist/primitives/threadListItem.js +3 -3
  55. package/dist/primitives/threadListItem.js.map +1 -1
  56. package/package.json +8 -7
  57. package/src/index.ts +22 -1
  58. package/src/primitives/actionBar/ActionBar.test.tsx +45 -0
  59. package/src/primitives/actionBar/ActionBarCopy.tsx +12 -3
  60. package/src/primitives/actionBar/ActionBarFeedbackNegative.tsx +13 -3
  61. package/src/primitives/actionBar/ActionBarFeedbackPositive.tsx +13 -3
  62. package/src/primitives/composer/ComposerInput.test.tsx +64 -4
  63. package/src/primitives/composer/ComposerInput.tsx +13 -2
  64. package/src/primitives/message/MessageContent.runtime.test.tsx +35 -0
  65. package/src/primitives/message/MessageContent.test.tsx +68 -4
  66. package/src/primitives/message/MessageContent.tsx +19 -9
  67. package/src/primitives/message/MessageParts.test.tsx +68 -1
  68. package/src/primitives/message/MessageParts.tsx +17 -1
  69. package/src/primitives/message.test.tsx +74 -0
  70. package/src/primitives/message.ts +1 -0
  71. package/src/primitives/thread/ThreadMessages.test.tsx +28 -1
  72. package/src/primitives/thread/ThreadMessages.tsx +49 -343
  73. package/src/primitives/thread/ThreadRowsFlatList.test.tsx +253 -0
  74. package/src/primitives/thread/ThreadRowsFlatList.tsx +159 -0
  75. package/src/primitives/thread/flatListScroll.ts +467 -0
  76. package/src/primitives/thread.ts +5 -0
  77. package/src/primitives/threadListItem/ThreadListItemTitle.test.tsx +164 -0
  78. package/src/primitives/threadListItem/ThreadListItemTitle.tsx +23 -0
  79. 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
- ? { ...components, Text: components.Text ?? rnDefaultComponents.Text }
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: { messages: [] as Msg[] },
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