stream-chat-react-native-core 9.13.0 → 9.14.0
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/ai-components/conversation/package.json +6 -0
- package/lib/commonjs/a2ui/A2UISurface.js +38 -13
- package/lib/commonjs/a2ui/A2UISurface.js.map +1 -1
- package/lib/commonjs/ai-components/README.md +10 -1
- package/lib/commonjs/ai-components/conversation/AIConversation.js +17 -9
- package/lib/commonjs/ai-components/conversation/AIConversation.js.map +1 -1
- package/lib/commonjs/ai-components/conversation/index.js +0 -33
- package/lib/commonjs/ai-components/conversation/index.js.map +1 -1
- package/lib/commonjs/ai-components/conversation/list/useKeyboardOverlap.js +3 -4
- package/lib/commonjs/ai-components/conversation/list/useKeyboardOverlap.js.map +1 -1
- package/lib/commonjs/ai-components/conversation/useConversationRows.js.map +1 -1
- package/lib/commonjs/ai-components/conversation-state/controller/AIAgentError.js.map +1 -0
- package/lib/{module/ai-components/conversation → commonjs/ai-components/conversation-state}/controller/AIConversationController.js +7 -1
- package/lib/commonjs/ai-components/conversation-state/controller/AIConversationController.js.map +1 -0
- package/lib/commonjs/ai-components/conversation-state/controller/AIConversationExchange.js.map +1 -0
- package/lib/commonjs/ai-components/conversation-state/controller/index.js.map +1 -0
- package/lib/commonjs/ai-components/{conversation → conversation-state}/controller/offers.js +10 -4
- package/lib/commonjs/ai-components/conversation-state/controller/offers.js.map +1 -0
- package/lib/commonjs/ai-components/conversation-state/controller/types.js.map +1 -0
- package/lib/commonjs/ai-components/conversation-state/controller/useAIConversationState.js.map +1 -0
- package/lib/commonjs/ai-components/{conversation → conversation-state}/fromStreamMessage.js +1 -0
- package/lib/commonjs/ai-components/conversation-state/fromStreamMessage.js.map +1 -0
- package/lib/commonjs/ai-components/conversation-state/index.js +37 -0
- package/lib/commonjs/ai-components/conversation-state/index.js.map +1 -0
- package/lib/commonjs/ai-components/conversation-state/types.js.map +1 -0
- package/lib/commonjs/ai-components/generative-ui/AIUIEntrance.js +9 -5
- package/lib/commonjs/ai-components/generative-ui/AIUIEntrance.js.map +1 -1
- package/lib/commonjs/ai-components/index.js +4 -4
- package/lib/commonjs/ai-components/index.js.map +1 -1
- package/lib/commonjs/ai-components/internal/keyboard.js +40 -0
- package/lib/commonjs/ai-components/internal/keyboard.js.map +1 -0
- package/lib/commonjs/ai-components/message/AIAssistantMessage.js +1 -0
- package/lib/commonjs/ai-components/message/AIAssistantMessage.js.map +1 -1
- package/lib/commonjs/ai-components/message-composer/ActionButton.js +2 -1
- package/lib/commonjs/ai-components/message-composer/ActionButton.js.map +1 -1
- package/lib/commonjs/ai-components/message-composer/AttachmentButton.js +2 -2
- package/lib/commonjs/ai-components/message-composer/AttachmentButton.js.map +1 -1
- package/lib/commonjs/ai-components/message-composer/ComposerView.js +20 -10
- package/lib/commonjs/ai-components/message-composer/ComposerView.js.map +1 -1
- package/lib/commonjs/ai-components/message-composer/EditingMessageChip.js +9 -8
- package/lib/commonjs/ai-components/message-composer/EditingMessageChip.js.map +1 -1
- package/lib/commonjs/ai-components/message-parts/AIMessageParts.js +25 -2
- package/lib/commonjs/ai-components/message-parts/AIMessageParts.js.map +1 -1
- package/lib/commonjs/i18n/ar.json +1 -0
- package/lib/commonjs/i18n/en.json +1 -0
- package/lib/commonjs/i18n/es.json +1 -0
- package/lib/commonjs/i18n/fr.json +1 -0
- package/lib/commonjs/i18n/he.json +1 -0
- package/lib/commonjs/i18n/hi.json +1 -0
- package/lib/commonjs/i18n/it.json +1 -0
- package/lib/commonjs/i18n/ja.json +1 -0
- package/lib/commonjs/i18n/ko.json +1 -0
- package/lib/commonjs/i18n/nl.json +1 -0
- package/lib/commonjs/i18n/pt-br.json +1 -0
- package/lib/commonjs/i18n/ru.json +1 -0
- package/lib/commonjs/i18n/tr.json +1 -0
- package/lib/commonjs/version.json +1 -1
- package/lib/module/a2ui/A2UISurface.js +38 -13
- package/lib/module/a2ui/A2UISurface.js.map +1 -1
- package/lib/module/ai-components/README.md +10 -1
- package/lib/module/ai-components/conversation/AIConversation.js +17 -9
- package/lib/module/ai-components/conversation/AIConversation.js.map +1 -1
- package/lib/module/ai-components/conversation/index.js +0 -33
- package/lib/module/ai-components/conversation/index.js.map +1 -1
- package/lib/module/ai-components/conversation/list/useKeyboardOverlap.js +3 -4
- package/lib/module/ai-components/conversation/list/useKeyboardOverlap.js.map +1 -1
- package/lib/module/ai-components/conversation/useConversationRows.js.map +1 -1
- package/lib/module/ai-components/conversation-state/controller/AIAgentError.js.map +1 -0
- package/lib/{commonjs/ai-components/conversation → module/ai-components/conversation-state}/controller/AIConversationController.js +7 -1
- package/lib/module/ai-components/conversation-state/controller/AIConversationController.js.map +1 -0
- package/lib/module/ai-components/conversation-state/controller/AIConversationExchange.js.map +1 -0
- package/lib/module/ai-components/conversation-state/controller/index.js.map +1 -0
- package/lib/module/ai-components/{conversation → conversation-state}/controller/offers.js +10 -4
- package/lib/module/ai-components/conversation-state/controller/offers.js.map +1 -0
- package/lib/module/ai-components/conversation-state/controller/types.js.map +1 -0
- package/lib/module/ai-components/conversation-state/controller/useAIConversationState.js.map +1 -0
- package/lib/module/ai-components/{conversation → conversation-state}/fromStreamMessage.js +1 -0
- package/lib/module/ai-components/conversation-state/fromStreamMessage.js.map +1 -0
- package/lib/module/ai-components/conversation-state/index.js +37 -0
- package/lib/module/ai-components/conversation-state/index.js.map +1 -0
- package/lib/module/ai-components/conversation-state/types.js.map +1 -0
- package/lib/module/ai-components/generative-ui/AIUIEntrance.js +9 -5
- package/lib/module/ai-components/generative-ui/AIUIEntrance.js.map +1 -1
- package/lib/module/ai-components/index.js +4 -4
- package/lib/module/ai-components/index.js.map +1 -1
- package/lib/module/ai-components/internal/keyboard.js +40 -0
- package/lib/module/ai-components/internal/keyboard.js.map +1 -0
- package/lib/module/ai-components/message/AIAssistantMessage.js +1 -0
- package/lib/module/ai-components/message/AIAssistantMessage.js.map +1 -1
- package/lib/module/ai-components/message-composer/ActionButton.js +2 -1
- package/lib/module/ai-components/message-composer/ActionButton.js.map +1 -1
- package/lib/module/ai-components/message-composer/AttachmentButton.js +2 -2
- package/lib/module/ai-components/message-composer/AttachmentButton.js.map +1 -1
- package/lib/module/ai-components/message-composer/ComposerView.js +20 -10
- package/lib/module/ai-components/message-composer/ComposerView.js.map +1 -1
- package/lib/module/ai-components/message-composer/EditingMessageChip.js +9 -8
- package/lib/module/ai-components/message-composer/EditingMessageChip.js.map +1 -1
- package/lib/module/ai-components/message-parts/AIMessageParts.js +25 -2
- package/lib/module/ai-components/message-parts/AIMessageParts.js.map +1 -1
- package/lib/module/i18n/ar.json +1 -0
- package/lib/module/i18n/en.json +1 -0
- package/lib/module/i18n/es.json +1 -0
- package/lib/module/i18n/fr.json +1 -0
- package/lib/module/i18n/he.json +1 -0
- package/lib/module/i18n/hi.json +1 -0
- package/lib/module/i18n/it.json +1 -0
- package/lib/module/i18n/ja.json +1 -0
- package/lib/module/i18n/ko.json +1 -0
- package/lib/module/i18n/nl.json +1 -0
- package/lib/module/i18n/pt-br.json +1 -0
- package/lib/module/i18n/ru.json +1 -0
- package/lib/module/i18n/tr.json +1 -0
- package/lib/module/version.json +1 -1
- package/lib/typescript/a2ui/A2UISurface.d.ts.map +1 -1
- package/lib/typescript/ai-components/conversation/AIConversation.d.ts +7 -4
- package/lib/typescript/ai-components/conversation/AIConversation.d.ts.map +1 -1
- package/lib/typescript/ai-components/conversation/entries.d.ts +1 -1
- package/lib/typescript/ai-components/conversation/entries.d.ts.map +1 -1
- package/lib/typescript/ai-components/conversation/index.d.ts +0 -3
- package/lib/typescript/ai-components/conversation/index.d.ts.map +1 -1
- package/lib/typescript/ai-components/conversation/list/useKeyboardOverlap.d.ts.map +1 -1
- package/lib/typescript/ai-components/conversation/useConversationRows.d.ts +1 -1
- package/lib/typescript/ai-components/conversation/useConversationRows.d.ts.map +1 -1
- package/lib/typescript/ai-components/conversation-state/controller/AIAgentError.d.ts.map +1 -0
- package/lib/typescript/ai-components/conversation-state/controller/AIConversationController.d.ts.map +1 -0
- package/lib/typescript/ai-components/conversation-state/controller/AIConversationExchange.d.ts.map +1 -0
- package/lib/typescript/ai-components/conversation-state/controller/index.d.ts.map +1 -0
- package/lib/typescript/ai-components/{conversation → conversation-state}/controller/offers.d.ts +4 -2
- package/lib/typescript/ai-components/conversation-state/controller/offers.d.ts.map +1 -0
- package/lib/typescript/ai-components/conversation-state/controller/types.d.ts.map +1 -0
- package/lib/typescript/ai-components/conversation-state/controller/useAIConversationState.d.ts.map +1 -0
- package/lib/typescript/ai-components/conversation-state/fromStreamMessage.d.ts.map +1 -0
- package/lib/typescript/ai-components/conversation-state/index.d.ts +4 -0
- package/lib/typescript/ai-components/conversation-state/index.d.ts.map +1 -0
- package/lib/typescript/ai-components/conversation-state/types.d.ts.map +1 -0
- package/lib/typescript/ai-components/generative-ui/AIUIEntrance.d.ts +4 -3
- package/lib/typescript/ai-components/generative-ui/AIUIEntrance.d.ts.map +1 -1
- package/lib/typescript/ai-components/generative-ui/AIUIHost.d.ts +1 -1
- package/lib/typescript/ai-components/generative-ui/AIUIHost.d.ts.map +1 -1
- package/lib/typescript/ai-components/index.d.ts +1 -1
- package/lib/typescript/ai-components/index.d.ts.map +1 -1
- package/lib/typescript/ai-components/internal/keyboard.d.ts +9 -0
- package/lib/typescript/ai-components/internal/keyboard.d.ts.map +1 -0
- package/lib/typescript/ai-components/message/AIAssistantMessage.d.ts.map +1 -1
- package/lib/typescript/ai-components/message-composer/ActionButton.d.ts +1 -1
- package/lib/typescript/ai-components/message-composer/ActionButton.d.ts.map +1 -1
- package/lib/typescript/ai-components/message-composer/ComposerView.d.ts +10 -5
- package/lib/typescript/ai-components/message-composer/ComposerView.d.ts.map +1 -1
- package/lib/typescript/ai-components/message-parts/AIMessageParts.d.ts +3 -1
- package/lib/typescript/ai-components/message-parts/AIMessageParts.d.ts.map +1 -1
- package/lib/typescript/i18n/ar.json +1 -0
- package/lib/typescript/i18n/en.json +1 -0
- package/lib/typescript/i18n/es.json +1 -0
- package/lib/typescript/i18n/fr.json +1 -0
- package/lib/typescript/i18n/he.json +1 -0
- package/lib/typescript/i18n/hi.json +1 -0
- package/lib/typescript/i18n/it.json +1 -0
- package/lib/typescript/i18n/ja.json +1 -0
- package/lib/typescript/i18n/ko.json +1 -0
- package/lib/typescript/i18n/nl.json +1 -0
- package/lib/typescript/i18n/pt-br.json +1 -0
- package/lib/typescript/i18n/ru.json +1 -0
- package/lib/typescript/i18n/tr.json +1 -0
- package/lib/typescript/utils/i18n/Streami18n.d.ts +1 -0
- package/lib/typescript/utils/i18n/Streami18n.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/a2ui/A2UISurface.tsx +36 -7
- package/src/a2ui/__tests__/A2UIRenderer.test.tsx +45 -0
- package/src/ai-components/README.md +10 -1
- package/src/ai-components/__tests__/ComposerView.test.tsx +21 -1
- package/src/ai-components/__tests__/conversation.test.tsx +85 -3
- package/src/ai-components/__tests__/conversationExchange.test.ts +1 -1
- package/src/ai-components/__tests__/conversationRows.test.ts +1 -1
- package/src/ai-components/__tests__/conversationUI.test.tsx +21 -1
- package/src/ai-components/__tests__/messagePartViews.test.tsx +42 -0
- package/src/ai-components/__tests__/withoutKeyboardController.test.tsx +60 -0
- package/src/ai-components/conversation/AIConversation.tsx +37 -15
- package/src/ai-components/conversation/entries.ts +1 -1
- package/src/ai-components/conversation/index.ts +0 -3
- package/src/ai-components/conversation/list/useKeyboardOverlap.ts +3 -2
- package/src/ai-components/conversation/useConversationRows.ts +2 -1
- package/src/ai-components/{conversation → conversation-state}/controller/AIConversationController.ts +7 -2
- package/src/ai-components/{conversation → conversation-state}/controller/offers.ts +13 -3
- package/src/ai-components/{conversation → conversation-state}/fromStreamMessage.ts +3 -1
- package/src/ai-components/conversation-state/index.ts +3 -0
- package/src/ai-components/generative-ui/AIUIEntrance.tsx +11 -5
- package/src/ai-components/generative-ui/AIUIHost.ts +1 -1
- package/src/ai-components/index.ts +1 -1
- package/src/ai-components/internal/keyboard.tsx +34 -0
- package/src/ai-components/message/AIAssistantMessage.tsx +1 -0
- package/src/ai-components/message-composer/ActionButton.tsx +5 -4
- package/src/ai-components/message-composer/AttachmentButton.tsx +2 -2
- package/src/ai-components/message-composer/ComposerView.tsx +25 -11
- package/src/ai-components/message-composer/EditingMessageChip.tsx +8 -7
- package/src/ai-components/message-parts/AIMessageParts.tsx +28 -3
- package/src/i18n/ar.json +1 -0
- package/src/i18n/en.json +1 -0
- package/src/i18n/es.json +1 -0
- package/src/i18n/fr.json +1 -0
- package/src/i18n/he.json +1 -0
- package/src/i18n/hi.json +1 -0
- package/src/i18n/it.json +1 -0
- package/src/i18n/ja.json +1 -0
- package/src/i18n/ko.json +1 -0
- package/src/i18n/nl.json +1 -0
- package/src/i18n/pt-br.json +1 -0
- package/src/i18n/ru.json +1 -0
- package/src/i18n/tr.json +1 -0
- package/src/version.json +1 -1
- package/lib/commonjs/ai-components/conversation/controller/AIAgentError.js.map +0 -1
- package/lib/commonjs/ai-components/conversation/controller/AIConversationController.js.map +0 -1
- package/lib/commonjs/ai-components/conversation/controller/AIConversationExchange.js.map +0 -1
- package/lib/commonjs/ai-components/conversation/controller/index.js.map +0 -1
- package/lib/commonjs/ai-components/conversation/controller/offers.js.map +0 -1
- package/lib/commonjs/ai-components/conversation/controller/types.js.map +0 -1
- package/lib/commonjs/ai-components/conversation/controller/useAIConversationState.js.map +0 -1
- package/lib/commonjs/ai-components/conversation/fromStreamMessage.js.map +0 -1
- package/lib/commonjs/ai-components/conversation/types.js.map +0 -1
- package/lib/module/ai-components/conversation/controller/AIAgentError.js.map +0 -1
- package/lib/module/ai-components/conversation/controller/AIConversationController.js.map +0 -1
- package/lib/module/ai-components/conversation/controller/AIConversationExchange.js.map +0 -1
- package/lib/module/ai-components/conversation/controller/index.js.map +0 -1
- package/lib/module/ai-components/conversation/controller/offers.js.map +0 -1
- package/lib/module/ai-components/conversation/controller/types.js.map +0 -1
- package/lib/module/ai-components/conversation/controller/useAIConversationState.js.map +0 -1
- package/lib/module/ai-components/conversation/fromStreamMessage.js.map +0 -1
- package/lib/module/ai-components/conversation/types.js.map +0 -1
- package/lib/typescript/ai-components/conversation/controller/AIAgentError.d.ts.map +0 -1
- package/lib/typescript/ai-components/conversation/controller/AIConversationController.d.ts.map +0 -1
- package/lib/typescript/ai-components/conversation/controller/AIConversationExchange.d.ts.map +0 -1
- package/lib/typescript/ai-components/conversation/controller/index.d.ts.map +0 -1
- package/lib/typescript/ai-components/conversation/controller/offers.d.ts.map +0 -1
- package/lib/typescript/ai-components/conversation/controller/types.d.ts.map +0 -1
- package/lib/typescript/ai-components/conversation/controller/useAIConversationState.d.ts.map +0 -1
- package/lib/typescript/ai-components/conversation/fromStreamMessage.d.ts.map +0 -1
- package/lib/typescript/ai-components/conversation/types.d.ts.map +0 -1
- /package/lib/commonjs/ai-components/{conversation → conversation-state}/controller/AIAgentError.js +0 -0
- /package/lib/commonjs/ai-components/{conversation → conversation-state}/controller/AIConversationExchange.js +0 -0
- /package/lib/commonjs/ai-components/{conversation → conversation-state}/controller/index.js +0 -0
- /package/lib/commonjs/ai-components/{conversation → conversation-state}/controller/types.js +0 -0
- /package/lib/commonjs/ai-components/{conversation → conversation-state}/controller/useAIConversationState.js +0 -0
- /package/lib/commonjs/ai-components/{conversation → conversation-state}/types.js +0 -0
- /package/lib/module/ai-components/{conversation → conversation-state}/controller/AIAgentError.js +0 -0
- /package/lib/module/ai-components/{conversation → conversation-state}/controller/AIConversationExchange.js +0 -0
- /package/lib/module/ai-components/{conversation → conversation-state}/controller/index.js +0 -0
- /package/lib/module/ai-components/{conversation → conversation-state}/controller/types.js +0 -0
- /package/lib/module/ai-components/{conversation → conversation-state}/controller/useAIConversationState.js +0 -0
- /package/lib/module/ai-components/{conversation → conversation-state}/types.js +0 -0
- /package/lib/typescript/ai-components/{conversation → conversation-state}/controller/AIAgentError.d.ts +0 -0
- /package/lib/typescript/ai-components/{conversation → conversation-state}/controller/AIConversationController.d.ts +0 -0
- /package/lib/typescript/ai-components/{conversation → conversation-state}/controller/AIConversationExchange.d.ts +0 -0
- /package/lib/typescript/ai-components/{conversation → conversation-state}/controller/index.d.ts +0 -0
- /package/lib/typescript/ai-components/{conversation → conversation-state}/controller/types.d.ts +0 -0
- /package/lib/typescript/ai-components/{conversation → conversation-state}/controller/useAIConversationState.d.ts +0 -0
- /package/lib/typescript/ai-components/{conversation → conversation-state}/fromStreamMessage.d.ts +0 -0
- /package/lib/typescript/ai-components/{conversation → conversation-state}/types.d.ts +0 -0
- /package/src/ai-components/{conversation → conversation-state}/controller/AIAgentError.ts +0 -0
- /package/src/ai-components/{conversation → conversation-state}/controller/AIConversationExchange.ts +0 -0
- /package/src/ai-components/{conversation → conversation-state}/controller/index.ts +0 -0
- /package/src/ai-components/{conversation → conversation-state}/controller/types.ts +0 -0
- /package/src/ai-components/{conversation → conversation-state}/controller/useAIConversationState.ts +0 -0
- /package/src/ai-components/{conversation → conversation-state}/types.ts +0 -0
package/src/a2ui/A2UISurface.tsx
CHANGED
|
@@ -6,6 +6,7 @@ import React, {
|
|
|
6
6
|
useContext,
|
|
7
7
|
useLayoutEffect,
|
|
8
8
|
useMemo,
|
|
9
|
+
useState,
|
|
9
10
|
useSyncExternalStore,
|
|
10
11
|
} from 'react';
|
|
11
12
|
import { type AccessibilityProps, View as RNView, type ViewProps } from 'react-native';
|
|
@@ -147,7 +148,8 @@ const PlacedComponent = ({
|
|
|
147
148
|
/**
|
|
148
149
|
* One part of a surface: its view from the basic catalog, or the app's component of its type,
|
|
149
150
|
* coming in when it shows, with its weight and what the agent tells assistive technology. A part
|
|
150
|
-
* whose type no catalog has, or that is still on its way, shows nothing
|
|
151
|
+
* whose type no catalog has, or that is still on its way, shows nothing, and one that holds other
|
|
152
|
+
* parts, such as a card, shows once one of them does while the agent writes it.
|
|
151
153
|
*/
|
|
152
154
|
// The path of the data model a TextField writes what the person types to, when it is obscured,
|
|
153
155
|
// such as a password, which must never reach the agent.
|
|
@@ -161,6 +163,18 @@ const secretPathOf = (node: ComponentNode, resolved: Record<string, unknown>) =>
|
|
|
161
163
|
: undefined;
|
|
162
164
|
};
|
|
163
165
|
|
|
166
|
+
// Tells the part around a part that it shows.
|
|
167
|
+
const A2UIPartShowsContext = createContext<(() => void) | undefined>(undefined);
|
|
168
|
+
|
|
169
|
+
// Whether a part's props hold other parts, as a card's child or a column's children do.
|
|
170
|
+
const holdsParts = (value: unknown): boolean =>
|
|
171
|
+
isComponentNode(value) ||
|
|
172
|
+
(Array.isArray(value)
|
|
173
|
+
? value.some(holdsParts)
|
|
174
|
+
: isPlainObject(value) && Object.values(value).some(holdsParts));
|
|
175
|
+
|
|
176
|
+
const hiddenStyle = { display: 'none' } as const;
|
|
177
|
+
|
|
164
178
|
const A2UINode = memo(({ node }: { node: ComponentNode }) => {
|
|
165
179
|
const engine = useContext(A2UIEngineContext);
|
|
166
180
|
const axis = useContext(A2UIAxisContext);
|
|
@@ -172,11 +186,26 @@ const A2UINode = memo(({ node }: { node: ComponentNode }) => {
|
|
|
172
186
|
useLayoutEffect(() => {
|
|
173
187
|
if (secretPath && surface) keepSecretPath(surface, secretPath);
|
|
174
188
|
}, [secretPath, surface]);
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
const
|
|
179
|
-
|
|
189
|
+
const View = engine?.components[node.type];
|
|
190
|
+
const isApp = !View && !!engine?.appComponent(node.type);
|
|
191
|
+
const isReady = !!engine && !node.isPlaceholder && node.state === 'resolved' && (!!View || isApp);
|
|
192
|
+
const holds = useMemo(() => holdsParts(props), [props]);
|
|
193
|
+
const [hasShownPart, setHasShownPart] = useState(false);
|
|
194
|
+
const isWritten = !useContext(A2UIReplyContext)?.isStreaming;
|
|
195
|
+
const shows = isReady && (!holds || hasShownPart || isWritten);
|
|
196
|
+
const partShows = useContext(A2UIPartShowsContext);
|
|
197
|
+
useLayoutEffect(() => {
|
|
198
|
+
if (shows) partShows?.();
|
|
199
|
+
}, [partShows, shows]);
|
|
200
|
+
const onPartShown = useCallback(() => setHasShownPart(true), []);
|
|
201
|
+
const comesIn = useAIUIComesIn(shows);
|
|
202
|
+
if (!isReady) return null;
|
|
203
|
+
const content = (
|
|
204
|
+
<A2UIPartShowsContext.Provider value={onPartShown}>
|
|
205
|
+
{View ? <View node={node} props={props} /> : <PlacedComponent node={node} props={props} />}
|
|
206
|
+
</A2UIPartShowsContext.Provider>
|
|
207
|
+
);
|
|
208
|
+
if (!shows) return <RNView style={hiddenStyle}>{content}</RNView>;
|
|
180
209
|
// A weighted part shares the room its row leaves, as flex-grow from nothing does.
|
|
181
210
|
const weight = typeof props.weight === 'number' && axis === 'row' ? props.weight : undefined;
|
|
182
211
|
const Part = comesIn ? EnteringPart : ShownPart;
|
|
@@ -186,7 +215,7 @@ const A2UINode = memo(({ node }: { node: ComponentNode }) => {
|
|
|
186
215
|
style={weight !== undefined ? { flexBasis: 0, flexGrow: weight } : undefined}
|
|
187
216
|
testID={`a2ui-${node.componentId}`}
|
|
188
217
|
>
|
|
189
|
-
{
|
|
218
|
+
{content}
|
|
190
219
|
</Part>
|
|
191
220
|
);
|
|
192
221
|
});
|
|
@@ -427,6 +427,51 @@ describe('a reply written in A2UI', () => {
|
|
|
427
427
|
await waitFor(() => expect(screen.getByTestId('trip-map')).toBeTruthy());
|
|
428
428
|
});
|
|
429
429
|
|
|
430
|
+
it('holds back a card until something in it shows, while the agent writes it', async () => {
|
|
431
|
+
const { ui } = setup();
|
|
432
|
+
const view = (lines: string[], status = 'streaming') => (
|
|
433
|
+
<StreamTheme>
|
|
434
|
+
<AIUIView document={documentOf(lines, status)} host={ui} messageId='a1' />
|
|
435
|
+
</StreamTheme>
|
|
436
|
+
);
|
|
437
|
+
const written = [
|
|
438
|
+
{ children: ['title', 'card'], component: 'Column', id: 'root' },
|
|
439
|
+
{ component: 'Text', id: 'title', text: 'Fit check' },
|
|
440
|
+
{ child: 'list', component: 'Card', id: 'card' },
|
|
441
|
+
{ children: ['tag'], component: 'Column', id: 'list' },
|
|
442
|
+
];
|
|
443
|
+
const { rerender } = render(view(surfaceOf(written)));
|
|
444
|
+
await waitFor(() => expect(screen.getByTestId('a2ui-title')).toBeTruthy());
|
|
445
|
+
expect(screen.queryByTestId('a2ui-card')).toBeNull();
|
|
446
|
+
expect(screen.queryByTestId('a2ui-list')).toBeNull();
|
|
447
|
+
|
|
448
|
+
rerender(
|
|
449
|
+
view([
|
|
450
|
+
...surfaceOf(written),
|
|
451
|
+
line({
|
|
452
|
+
updateComponents: {
|
|
453
|
+
components: [{ component: 'Text', id: 'tag', text: 'Name tag' }],
|
|
454
|
+
surfaceId: 's',
|
|
455
|
+
},
|
|
456
|
+
}),
|
|
457
|
+
]),
|
|
458
|
+
);
|
|
459
|
+
await waitFor(() => expect(screen.getByTestId('a2ui-card')).toBeTruthy());
|
|
460
|
+
expect(screen.getByText('Name tag')).toBeTruthy();
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
it('shows a card that holds nothing yet once the agent has written the reply', async () => {
|
|
464
|
+
const { ui } = setup();
|
|
465
|
+
await renderReply(
|
|
466
|
+
ui,
|
|
467
|
+
surfaceOf([
|
|
468
|
+
{ child: 'list', component: 'Card', id: 'root' },
|
|
469
|
+
{ children: ['tag'], component: 'Column', id: 'list' },
|
|
470
|
+
]),
|
|
471
|
+
);
|
|
472
|
+
expect(screen.getByTestId('a2ui-root')).toBeTruthy();
|
|
473
|
+
});
|
|
474
|
+
|
|
430
475
|
it('shows a placeholder while the first surface is being written', async () => {
|
|
431
476
|
const { ui } = setup();
|
|
432
477
|
await renderReply(ui, ['{"version":"1.0","createSur'], 'streaming');
|
|
@@ -36,6 +36,14 @@ yarn add victory-native @shopify/react-native-skia
|
|
|
36
36
|
|
|
37
37
|
`react-native-reanimated`, `react-native-gesture-handler` and `react-native-svg` are already required by the chat SDK.
|
|
38
38
|
|
|
39
|
+
`AIConversation` comes from its own entry point, `stream-chat-react-native/ai-components/conversation` (or `stream-chat-expo/ai-components/conversation`), and needs [`react-native-keyboard-controller`](https://kirillzyusko.github.io/react-native-keyboard-controller/) with its `KeyboardProvider` above it:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
yarn add react-native-keyboard-controller
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The rest of the AI components work without it. With it installed and a `KeyboardProvider` above them, `ComposerView` rides the keyboard up; without it, the composer doesn't move with the keyboard.
|
|
46
|
+
|
|
39
47
|
The `zod` dependency ships `export * as` syntax, which the React Native Babel preset does not transform, so add the `export-namespace-from` plugin to your `babel.config.js`:
|
|
40
48
|
|
|
41
49
|
```bash
|
|
@@ -299,7 +307,8 @@ An agent can answer with UI instead of text: a form, a list of options, a card w
|
|
|
299
307
|
|
|
300
308
|
```tsx
|
|
301
309
|
import { createA2UIRenderer } from 'stream-chat-react-native/a2ui'; // or 'stream-chat-expo/a2ui'
|
|
302
|
-
import {
|
|
310
|
+
import { AIUIHost, aiUIActionJSON } from 'stream-chat-react-native/ai-components';
|
|
311
|
+
import { AIConversation } from 'stream-chat-react-native/ai-components/conversation';
|
|
303
312
|
|
|
304
313
|
const Conversation = ({ messages, session }) => {
|
|
305
314
|
// One host per conversation, kept for as long as it shows, so what a reply's UI holds (a
|
|
@@ -238,6 +238,26 @@ describe('ComposerView trailing', () => {
|
|
|
238
238
|
});
|
|
239
239
|
});
|
|
240
240
|
|
|
241
|
+
describe('ComposerView SpeechToTextButton', () => {
|
|
242
|
+
const Dictate = () => <Text>dictate</Text>;
|
|
243
|
+
|
|
244
|
+
it('shows the one it is given while the message box is empty, and the send button after', () => {
|
|
245
|
+
renderComposer({ SpeechToTextButton: Dictate });
|
|
246
|
+
expect(screen.getByText('dictate')).toBeTruthy();
|
|
247
|
+
|
|
248
|
+
fireEvent.changeText(screen.getByPlaceholderText('Ask anything'), 'Hello');
|
|
249
|
+
expect(screen.queryByText('dictate')).toBeNull();
|
|
250
|
+
expect(screen.getByTestId('send-up')).toBeTruthy();
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('shows none with null', () => {
|
|
254
|
+
renderComposer({ SpeechToTextButton: null });
|
|
255
|
+
expect(screen.queryByText('dictate')).toBeNull();
|
|
256
|
+
expect(screen.queryByTestId('send-up')).toBeNull();
|
|
257
|
+
expect(screen.getByPlaceholderText('Ask anything')).toBeTruthy();
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
241
261
|
describe('AISuggestions', () => {
|
|
242
262
|
it('hands the picked suggestion over, and shows nothing without any', () => {
|
|
243
263
|
const onSelect = jest.fn();
|
|
@@ -325,7 +345,7 @@ describe('ComposerView with a conversation', () => {
|
|
|
325
345
|
|
|
326
346
|
act(() => controller.edit(message));
|
|
327
347
|
|
|
328
|
-
expect(screen.getByText('Editing
|
|
348
|
+
expect(screen.getByText('Editing')).toBeTruthy();
|
|
329
349
|
expect(screen.getByPlaceholderText('Ask anything').props.value).toBe('Plan Lisbon');
|
|
330
350
|
|
|
331
351
|
fireEvent.press(screen.getByRole('button', { name: 'Cancel editing' }));
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import React, { type ReactElement, useEffect } from 'react';
|
|
2
|
-
import { Text } from 'react-native';
|
|
2
|
+
import { StyleSheet, Text } from 'react-native';
|
|
3
3
|
|
|
4
4
|
import { act, fireEvent, render, screen } from '@testing-library/react-native';
|
|
5
5
|
|
|
6
|
-
import { AIPendingMessageTracker, turnsOf } from '../conversation/entries';
|
|
7
6
|
import {
|
|
8
7
|
AIConversation,
|
|
9
8
|
type AIConversationAssistantMessageProps,
|
|
9
|
+
type AIConversationUserMessageProps,
|
|
10
|
+
} from '../conversation';
|
|
11
|
+
import { AIPendingMessageTracker, turnsOf } from '../conversation/entries';
|
|
12
|
+
import {
|
|
10
13
|
AIConversationController,
|
|
11
14
|
type AIConversationControllerOptions,
|
|
12
15
|
type AIConversationFeed,
|
|
13
16
|
type AIConversationTransport,
|
|
14
|
-
type AIConversationUserMessageProps,
|
|
15
17
|
type AIMessage,
|
|
16
18
|
type AIToolApprover,
|
|
17
19
|
createFromStreamMessage,
|
|
@@ -337,6 +339,44 @@ describe('AIConversation', () => {
|
|
|
337
339
|
expect(screen.getByText('actions')).toBeTruthy();
|
|
338
340
|
});
|
|
339
341
|
|
|
342
|
+
it('keeps the latest reply at its tallest, and lets the person’s message shrink', async () => {
|
|
343
|
+
const UserMessage = ({ text }: AIConversationUserMessageProps) => <Text>{text}</Text>;
|
|
344
|
+
const AssistantMessage = ({ text }: AIConversationAssistantMessageProps) => <Text>{text}</Text>;
|
|
345
|
+
await renderConversation(
|
|
346
|
+
<AIConversation
|
|
347
|
+
AssistantMessage={AssistantMessage}
|
|
348
|
+
messages={[user('u1', 'Hi'), reply('a1', 'Hello')]}
|
|
349
|
+
ReplyActions={null}
|
|
350
|
+
UserMessage={UserMessage}
|
|
351
|
+
/>,
|
|
352
|
+
);
|
|
353
|
+
// The row is the view around the message with the list's side and top gaps.
|
|
354
|
+
const rowOf = (text: string) => {
|
|
355
|
+
let node = screen.getByText(text).parent;
|
|
356
|
+
while (node) {
|
|
357
|
+
const style = typeof node.type === 'string' ? StyleSheet.flatten(node.props.style) : null;
|
|
358
|
+
if (style?.paddingHorizontal !== undefined && style.paddingTop !== undefined) return node;
|
|
359
|
+
node = node.parent;
|
|
360
|
+
}
|
|
361
|
+
return null;
|
|
362
|
+
};
|
|
363
|
+
const minHeightOf = (text: string) =>
|
|
364
|
+
StyleSheet.flatten(rowOf(text)?.props.style)?.minHeight ?? 0;
|
|
365
|
+
const layOutRow = (text: string, height: number) =>
|
|
366
|
+
act(() => {
|
|
367
|
+
rowOf(text)?.props.onLayout?.(layoutOf(height));
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
layOutRow('Hello', 120);
|
|
371
|
+
layOutRow('Hello', 60);
|
|
372
|
+
expect(minHeightOf('Hello')).toBe(120);
|
|
373
|
+
|
|
374
|
+
layOutRow('Hi', 120);
|
|
375
|
+
layOutRow('Hi', 60);
|
|
376
|
+
expect(rowOf('Hi')?.props.onLayout).toBeUndefined();
|
|
377
|
+
expect(minHeightOf('Hi')).toBe(0);
|
|
378
|
+
});
|
|
379
|
+
|
|
340
380
|
it('renders again only the messages that changed', async () => {
|
|
341
381
|
const renders: string[] = [];
|
|
342
382
|
const AssistantMessage = ({ message, text }: AIConversationAssistantMessageProps) => {
|
|
@@ -541,6 +581,27 @@ describe('fromStreamMessage', () => {
|
|
|
541
581
|
expect(reasoningText).toHaveBeenCalledWith('m1', 'r1');
|
|
542
582
|
});
|
|
543
583
|
|
|
584
|
+
it('leaves out the link previews Stream made from the text', () => {
|
|
585
|
+
const message = fromStreamMessage({
|
|
586
|
+
ai_generated: true,
|
|
587
|
+
attachments: [
|
|
588
|
+
{
|
|
589
|
+
image_url: 'https://cal.com/og/index.png',
|
|
590
|
+
og_scrape_url: 'Cal.com',
|
|
591
|
+
title: 'Cal.com | Scheduling Software for Online Bookings',
|
|
592
|
+
title_link: 'https://cal.com',
|
|
593
|
+
type: 'image',
|
|
594
|
+
},
|
|
595
|
+
{ image_url: 'https://cdn.example.com/a.jpg', title: 'a.jpg', type: 'image' },
|
|
596
|
+
],
|
|
597
|
+
id: 'm1',
|
|
598
|
+
text: 'Book it on Cal.com',
|
|
599
|
+
});
|
|
600
|
+
expect(message.attachments).toEqual([
|
|
601
|
+
{ kind: 'image', mimeType: undefined, name: 'a.jpg', url: 'https://cdn.example.com/a.jpg' },
|
|
602
|
+
]);
|
|
603
|
+
});
|
|
604
|
+
|
|
544
605
|
it('adapts the person’s message', () => {
|
|
545
606
|
expect(fromStreamMessage({ attachments: [], id: 'u1', text: 'Hi' })).toMatchObject({
|
|
546
607
|
isGenerating: false,
|
|
@@ -613,6 +674,27 @@ describe('AIConversation with a controller', () => {
|
|
|
613
674
|
controller.dispose();
|
|
614
675
|
});
|
|
615
676
|
|
|
677
|
+
it('leaves the other messages alone while a reply is written', async () => {
|
|
678
|
+
let renders = 0;
|
|
679
|
+
const UserMessage = ({ text }: AIConversationUserMessageProps) => {
|
|
680
|
+
renders++;
|
|
681
|
+
return <Text>{text}</Text>;
|
|
682
|
+
};
|
|
683
|
+
const history = [user('u1', 'Hello'), reply('a1', 'Hi!'), user('u2', 'Plan my day')];
|
|
684
|
+
const { controller, update } = conversationOf(history);
|
|
685
|
+
await renderConversation(
|
|
686
|
+
<AIConversation conversation={controller} UserMessage={UserMessage} />,
|
|
687
|
+
);
|
|
688
|
+
await update([...history, reply('a2', 'Here', { isGenerating: true })]);
|
|
689
|
+
const before = renders;
|
|
690
|
+
for (const text of ['Here is', 'Here is your', 'Here is your plan.']) {
|
|
691
|
+
await update([...history, reply('a2', text, { isGenerating: true })]);
|
|
692
|
+
}
|
|
693
|
+
expect(screen.getByText('Here is your plan.')).toBeTruthy();
|
|
694
|
+
expect(renders).toBe(before);
|
|
695
|
+
controller.dispose();
|
|
696
|
+
});
|
|
697
|
+
|
|
616
698
|
it('shows the agent thinking while a reply is written again', async () => {
|
|
617
699
|
const { controller } = conversationOf([user('u1', 'Hello'), reply('a1', 'Hi!')]);
|
|
618
700
|
await renderConversation(<AIConversation conversation={controller} />);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AIConversationRequest, AIMessage } from '..';
|
|
2
|
-
import { AIConversationExchange } from '../conversation/controller/AIConversationExchange';
|
|
2
|
+
import { AIConversationExchange } from '../conversation-state/controller/AIConversationExchange';
|
|
3
3
|
|
|
4
4
|
const user = (id: string, text = 'Plan a weekend in Lisbon'): AIMessage => ({
|
|
5
5
|
id,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { renderHook } from '@testing-library/react-native';
|
|
2
2
|
|
|
3
|
-
import type { AIMessage, AIPendingMessage } from '../conversation/types';
|
|
4
3
|
import {
|
|
5
4
|
type ConversationRow,
|
|
6
5
|
useConversationRows,
|
|
7
6
|
type UseConversationRowsOptions,
|
|
8
7
|
} from '../conversation/useConversationRows';
|
|
8
|
+
import type { AIMessage, AIPendingMessage } from '../conversation-state/types';
|
|
9
9
|
import type { AIMessagePartData } from '../message-parts/types';
|
|
10
10
|
|
|
11
11
|
const user = (id: string, text: string): AIMessage => ({ id, role: 'user', text });
|
|
@@ -3,8 +3,8 @@ import { Pressable, StyleSheet, Text, View } from 'react-native';
|
|
|
3
3
|
|
|
4
4
|
import { act, fireEvent, render, screen } from '@testing-library/react-native';
|
|
5
5
|
|
|
6
|
+
import { AIConversation } from '../conversation';
|
|
6
7
|
import {
|
|
7
|
-
AIConversation,
|
|
8
8
|
AIConversationController,
|
|
9
9
|
type AIMessage,
|
|
10
10
|
AIUIComponentView,
|
|
@@ -264,6 +264,26 @@ describe('AIConversation with generated UI', () => {
|
|
|
264
264
|
expect(stageOpacityOf('Being written')).toBe(0);
|
|
265
265
|
});
|
|
266
266
|
|
|
267
|
+
it('brings in a UI that arrives whole once a reply seen being written finishes', async () => {
|
|
268
|
+
const ui = makeHost();
|
|
269
|
+
const { rerender } = await renderConversation(
|
|
270
|
+
<AIConversation
|
|
271
|
+
messages={[user('u1', 'Plan'), reply('a1', 'Planning', undefined, { generating: true })]}
|
|
272
|
+
ui={ui}
|
|
273
|
+
/>,
|
|
274
|
+
);
|
|
275
|
+
await rerender(
|
|
276
|
+
<AIConversation
|
|
277
|
+
messages={[
|
|
278
|
+
user('u1', 'Plan'),
|
|
279
|
+
reply('a1', '', { content: 'Arrived whole', format: 'a2ui' }),
|
|
280
|
+
]}
|
|
281
|
+
ui={ui}
|
|
282
|
+
/>,
|
|
283
|
+
);
|
|
284
|
+
expect(stageOpacityOf('Arrived whole')).toBe(0);
|
|
285
|
+
});
|
|
286
|
+
|
|
267
287
|
it('hands the replies’ documents to the host as they change', async () => {
|
|
268
288
|
const ui = makeHost();
|
|
269
289
|
const receive = jest.spyOn(ui, 'receive');
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
StreamTheme,
|
|
19
19
|
} from '../index';
|
|
20
20
|
import { forgetCallStates } from '../message-parts/callStates';
|
|
21
|
+
import { AIThinkingClock, AIThinkingTurnContext } from '../reasoning/thinkingClock';
|
|
21
22
|
|
|
22
23
|
// Each test's questions start unanswered, though tests reuse calls.
|
|
23
24
|
afterEach(forgetCallStates);
|
|
@@ -326,6 +327,47 @@ describe('AIMessageParts', () => {
|
|
|
326
327
|
version: 1,
|
|
327
328
|
});
|
|
328
329
|
|
|
330
|
+
// Whether the step a view is of comes in: an entrance on the way up to the list of steps.
|
|
331
|
+
const comesIn = (testID: string) => {
|
|
332
|
+
let node = screen.getByTestId(testID).parent;
|
|
333
|
+
while (node && node.props.testID !== 'ai-message-parts') {
|
|
334
|
+
if (node.props.entering) return true;
|
|
335
|
+
node = node.parent;
|
|
336
|
+
}
|
|
337
|
+
return false;
|
|
338
|
+
};
|
|
339
|
+
const call = (id: string): AIMessagePartData => ({
|
|
340
|
+
id,
|
|
341
|
+
kind: 'ai_tool_call',
|
|
342
|
+
payload: {},
|
|
343
|
+
toolCall: toolCall({ id }),
|
|
344
|
+
version: 1,
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('brings in the steps of a reply being written, and shows those of one written before', () => {
|
|
348
|
+
const { rerender } = renderWithTheme(
|
|
349
|
+
<AIMessageParts isGenerating messageId='m1' parts={[call('c1')]} />,
|
|
350
|
+
);
|
|
351
|
+
expect(comesIn('ai-tool-call')).toBe(true);
|
|
352
|
+
|
|
353
|
+
rerender(<AIMessageParts messageId='m1' parts={[call('c1')]} />);
|
|
354
|
+
expect(comesIn('ai-tool-call')).toBe(true);
|
|
355
|
+
|
|
356
|
+
renderWithTheme(<AIMessageParts messageId='m2' parts={[reasoning('r1', 'Done')]} />);
|
|
357
|
+
expect(comesIn('ai-reasoning')).toBe(false);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
it('shows at once the step that carries on the agent’s thinking', () => {
|
|
361
|
+
const clock = new AIThinkingClock();
|
|
362
|
+
clock.update('t1', true, false, false);
|
|
363
|
+
renderWithTheme(
|
|
364
|
+
<AIThinkingTurnContext.Provider value={{ clock, firstStepId: 'r1', key: 't1' }}>
|
|
365
|
+
<AIMessageParts isGenerating messageId='m1' parts={[reasoning('r1', 'Hmm')]} />
|
|
366
|
+
</AIThinkingTurnContext.Provider>,
|
|
367
|
+
);
|
|
368
|
+
expect(comesIn('ai-reasoning')).toBe(false);
|
|
369
|
+
});
|
|
370
|
+
|
|
329
371
|
it('updates its views for another message’s steps, each starting afresh', () => {
|
|
330
372
|
const { rerender } = renderWithTheme(
|
|
331
373
|
<AIMessageParts messageId='m1' parts={[reasoning('r1', 'First')]} />,
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Keyboard, Text } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { act, fireEvent, render, screen } from '@testing-library/react-native';
|
|
5
|
+
|
|
6
|
+
import { ComposerView, sheetStore, sheetStoreApi, StreamTheme } from '../index';
|
|
7
|
+
import { dismissKeyboard, KeyboardStickyView, keyboardHeight } from '../internal/keyboard';
|
|
8
|
+
|
|
9
|
+
jest.mock('react-native-keyboard-controller', () => {
|
|
10
|
+
throw new Error("Cannot find module 'react-native-keyboard-controller'");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const flushPromises = () => new Promise<void>((resolve) => setTimeout(resolve, 0));
|
|
14
|
+
|
|
15
|
+
describe('without react-native-keyboard-controller', () => {
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
act(() => {
|
|
18
|
+
sheetStoreApi.closeSheet();
|
|
19
|
+
});
|
|
20
|
+
jest.restoreAllMocks();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('lays the sticky view out as a plain view', () => {
|
|
24
|
+
render(
|
|
25
|
+
<KeyboardStickyView offset={{ closed: 0, opened: 34 }} testID='sticky'>
|
|
26
|
+
<Text>inside</Text>
|
|
27
|
+
</KeyboardStickyView>,
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
const sticky = screen.getByTestId('sticky');
|
|
31
|
+
expect(sticky.type).toBe('View');
|
|
32
|
+
expect(sticky.props.offset).toBeUndefined();
|
|
33
|
+
expect(screen.getByText('inside')).toBeTruthy();
|
|
34
|
+
expect(keyboardHeight()).toBe(0);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('closes the keyboard with React Native', async () => {
|
|
38
|
+
const dismiss = jest.spyOn(Keyboard, 'dismiss');
|
|
39
|
+
|
|
40
|
+
await expect(dismissKeyboard()).resolves.toBeUndefined();
|
|
41
|
+
expect(dismiss).toHaveBeenCalled();
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('renders the composer and opens its sheet', async () => {
|
|
45
|
+
const dismiss = jest.spyOn(Keyboard, 'dismiss');
|
|
46
|
+
render(
|
|
47
|
+
<StreamTheme>
|
|
48
|
+
<ComposerView onSendMessage={() => Promise.resolve()} />
|
|
49
|
+
</StreamTheme>,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
await act(async () => {
|
|
53
|
+
fireEvent.press(screen.getByText('+'));
|
|
54
|
+
await flushPromises();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
expect(dismiss).toHaveBeenCalled();
|
|
58
|
+
expect(sheetStore.getLatestValue().open).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -12,18 +12,22 @@ import Animated, { Easing, FadeIn } from 'react-native-reanimated';
|
|
|
12
12
|
|
|
13
13
|
import type { LegendListRenderItemProps } from '@legendapp/list/react-native';
|
|
14
14
|
|
|
15
|
-
import type {
|
|
16
|
-
AIConversationController,
|
|
17
|
-
AIConversationControllerState,
|
|
18
|
-
} from './controller/AIConversationController';
|
|
19
|
-
import { editActionOf, replyActionsOf } from './controller/offers';
|
|
20
|
-
import { useAIConversationState } from './controller/useAIConversationState';
|
|
21
15
|
import type { AIConversationEntry } from './entries';
|
|
22
16
|
import { AIChatList } from './list/AIChatList';
|
|
23
|
-
import type { AIMessage, AIPendingMessage } from './types';
|
|
24
17
|
import { type ConversationRow, type RowPosition, useConversationRows } from './useConversationRows';
|
|
25
18
|
|
|
26
19
|
import { useTheme } from '../contexts';
|
|
20
|
+
import type {
|
|
21
|
+
AIConversationController,
|
|
22
|
+
AIConversationControllerState,
|
|
23
|
+
} from '../conversation-state/controller/AIConversationController';
|
|
24
|
+
import {
|
|
25
|
+
editActionOf,
|
|
26
|
+
latestUserMessageIdOf,
|
|
27
|
+
replyActionsOf,
|
|
28
|
+
} from '../conversation-state/controller/offers';
|
|
29
|
+
import { useAIConversationState } from '../conversation-state/controller/useAIConversationState';
|
|
30
|
+
import type { AIMessage, AIPendingMessage } from '../conversation-state/types';
|
|
27
31
|
import type { AIUIHost } from '../generative-ui/AIUIHost';
|
|
28
32
|
import { AIUIGenerated, useAIUIHostRefresh } from '../generative-ui/AIUIView';
|
|
29
33
|
import { primitives } from '../internal/tokens';
|
|
@@ -103,8 +107,7 @@ export type AIConversationProps = {
|
|
|
103
107
|
* How far above the bottom of the screen the composer sits, such as above a tab bar. The
|
|
104
108
|
* keyboard comes up over the conversation, which makes room for it below the latest turn (the
|
|
105
109
|
* room the pinned question leaves takes it first), and covers it only as far as it rises past
|
|
106
|
-
* this. Defaults to the bottom safe area.
|
|
107
|
-
* react-native-keyboard-controller above it.
|
|
110
|
+
* this. Defaults to the bottom safe area.
|
|
108
111
|
*/
|
|
109
112
|
keyboardOffset?: number;
|
|
110
113
|
/**
|
|
@@ -182,6 +185,8 @@ const DefaultAssistantMessage = ({
|
|
|
182
185
|
// shrinks (a sign-in going, reasoning folding, the thinking going) only a frame later, and in that
|
|
183
186
|
// frame the question would be pulled down. Below a pinned turn there is room anyway, so it looks
|
|
184
187
|
// the same, and the room stays the list's, which the keyboard takes before it lifts anything.
|
|
188
|
+
// The person's own message is left free: something folding in it, such as a transcript, would
|
|
189
|
+
// leave a gap above the reply.
|
|
185
190
|
type TurnFloor = { heights: Map<string, number>; turnKey: string | undefined };
|
|
186
191
|
|
|
187
192
|
type ConversationContextValue = {
|
|
@@ -224,6 +229,7 @@ type EntryViewProps = {
|
|
|
224
229
|
const NO_MESSAGES: AIMessage[] = [];
|
|
225
230
|
|
|
226
231
|
const REPLY_ACTIONS_IN = FadeIn.duration(200).easing(Easing.out(Easing.ease));
|
|
232
|
+
const THINKING_IN = REPLY_ACTIONS_IN;
|
|
227
233
|
|
|
228
234
|
const useReplyWriting = (messageId: string | undefined, isGenerating: boolean) => {
|
|
229
235
|
const [writing, setWriting] = useState<string>();
|
|
@@ -295,14 +301,18 @@ const EntryView = ({
|
|
|
295
301
|
/>
|
|
296
302
|
{showsUI ? (
|
|
297
303
|
<AIUIGenerated
|
|
298
|
-
isStreaming={uiDocument.isStreaming}
|
|
304
|
+
isStreaming={uiDocument.isStreaming || wasWatched}
|
|
299
305
|
messageId={message.id}
|
|
300
306
|
onError={(error) => ui?.fail(message.id, error)}
|
|
301
307
|
>
|
|
302
308
|
{generated}
|
|
303
309
|
</AIUIGenerated>
|
|
304
310
|
) : null}
|
|
305
|
-
{thinkingSince !== undefined ?
|
|
311
|
+
{thinkingSince !== undefined ? (
|
|
312
|
+
<Animated.View entering={THINKING_IN}>
|
|
313
|
+
<AIThinking since={thinkingSince} />
|
|
314
|
+
</Animated.View>
|
|
315
|
+
) : null}
|
|
306
316
|
{ReplyActions && !isGenerating && !isWriting && hasAnswer && !isWritingUI ? (
|
|
307
317
|
<Animated.View entering={wasWatched ? REPLY_ACTIONS_IN : undefined}>
|
|
308
318
|
<ReplyActions isLatestReply={entry.isLatestReply} message={message} text={message.text} />
|
|
@@ -328,7 +338,10 @@ const Row = memo(({ row }: { row: ConversationRow }) => {
|
|
|
328
338
|
// a worklet sent to the UI thread. The floor only rises, long before a row shrinks, so it can
|
|
329
339
|
// follow a frame after the row grows.
|
|
330
340
|
const turnFloor = context?.turnFloor;
|
|
331
|
-
const isHeld =
|
|
341
|
+
const isHeld =
|
|
342
|
+
!!turnFloor &&
|
|
343
|
+
row.turnKey === turnFloor.turnKey &&
|
|
344
|
+
!(row.kind === 'entry' && row.entry.isFromUser);
|
|
332
345
|
const floor = isHeld ? (turnFloor?.heights.get(row.key) ?? 0) : 0;
|
|
333
346
|
const [, setFloorRaised] = useState(0);
|
|
334
347
|
const onLayout = isHeld
|
|
@@ -357,7 +370,11 @@ const Row = memo(({ row }: { row: ConversationRow }) => {
|
|
|
357
370
|
/>
|
|
358
371
|
) : (
|
|
359
372
|
<>
|
|
360
|
-
{row.isThinking ?
|
|
373
|
+
{row.isThinking ? (
|
|
374
|
+
<Animated.View entering={THINKING_IN}>
|
|
375
|
+
<AIThinking />
|
|
376
|
+
</Animated.View>
|
|
377
|
+
) : null}
|
|
361
378
|
{row.footer}
|
|
362
379
|
</>
|
|
363
380
|
)}
|
|
@@ -423,6 +440,10 @@ const DefaultConversationReplyActions = ({
|
|
|
423
440
|
* - A button scrolls back to the latest message when the end is out of view.
|
|
424
441
|
* - A message the person sent shows at once (`pendingMessage`) and the agent thinks under it
|
|
425
442
|
* (`AIThinking`) until its reply shows something.
|
|
443
|
+
*
|
|
444
|
+
* Import it from `stream-chat-react-native/ai-components/conversation` (or
|
|
445
|
+
* `stream-chat-expo/ai-components/conversation`). It needs react-native-keyboard-controller, with
|
|
446
|
+
* its `KeyboardProvider` above it.
|
|
426
447
|
*/
|
|
427
448
|
export const AIConversation = (props: AIConversationProps) =>
|
|
428
449
|
props.conversation ? (
|
|
@@ -461,9 +482,10 @@ const ControlledConversation = ({
|
|
|
461
482
|
replyActionsOf(conversation, { ratings, status }, message, isLatestReply),
|
|
462
483
|
[conversation, ratings, status],
|
|
463
484
|
);
|
|
485
|
+
const latestUserMessageId = latestUserMessageIdOf(messages);
|
|
464
486
|
const editAction = useMemo<EditAction>(
|
|
465
|
-
() => (message) => editActionOf(conversation, {
|
|
466
|
-
[conversation,
|
|
487
|
+
() => (message) => editActionOf(conversation, { latestUserMessageId, status }, message),
|
|
488
|
+
[conversation, latestUserMessageId, status],
|
|
467
489
|
);
|
|
468
490
|
const { ui } = props;
|
|
469
491
|
// A reply that shows only its UI has started, which the controller can't tell without the
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AIMessage, AIPendingMessage } from '
|
|
1
|
+
import type { AIMessage, AIPendingMessage } from '../conversation-state/types';
|
|
2
2
|
|
|
3
3
|
import type { AIUIDocument } from '../generative-ui/AIUIDocument';
|
|
4
4
|
import type { AIMessageAttachmentData } from '../message/AIMessageAttachment';
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { useRef } from 'react';
|
|
2
|
-
import { KeyboardController, useKeyboardHandler } from 'react-native-keyboard-controller';
|
|
3
2
|
import { runOnJS, type SharedValue, useSharedValue } from 'react-native-reanimated';
|
|
4
3
|
|
|
4
|
+
import { keyboardHeight, useKeyboardHandler } from '../../internal/keyboard';
|
|
5
|
+
|
|
5
6
|
export type KeyboardFrame = { height: number; progress: number };
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -19,7 +20,7 @@ export const keyboardOverlapOf = (keyboard: KeyboardFrame, offset: number) => {
|
|
|
19
20
|
* once it has finished moving.
|
|
20
21
|
*/
|
|
21
22
|
export const useKeyboardFrame = (onSettled?: () => void): SharedValue<KeyboardFrame> => {
|
|
22
|
-
const initial =
|
|
23
|
+
const initial = keyboardHeight();
|
|
23
24
|
const keyboard = useSharedValue<KeyboardFrame>({
|
|
24
25
|
height: initial,
|
|
25
26
|
progress: initial > 0 ? 1 : 0,
|
|
@@ -8,7 +8,8 @@ import {
|
|
|
8
8
|
hasFinishedSteps,
|
|
9
9
|
turnsOf,
|
|
10
10
|
} from './entries';
|
|
11
|
-
|
|
11
|
+
|
|
12
|
+
import type { AIMessage, AIPendingMessage } from '../conversation-state/types';
|
|
12
13
|
|
|
13
14
|
import type { AIMessageAttachmentData } from '../message/AIMessageAttachment';
|
|
14
15
|
import { AIThinkingClock } from '../reasoning/thinkingClock';
|