@aws-amplify/ui-react-ai 0.4.0 → 1.1.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/dist/esm/components/AIConversation/AIConversation.mjs +8 -26
- package/dist/esm/components/AIConversation/AIConversationProvider.mjs +20 -17
- package/dist/esm/components/AIConversation/context/AIContextContext.mjs +8 -0
- package/dist/esm/components/AIConversation/context/AttachmentContext.mjs +12 -3
- package/dist/esm/components/AIConversation/context/ConversationInputContext.mjs +2 -1
- package/dist/esm/components/AIConversation/context/FallbackComponentContext.mjs +8 -0
- package/dist/esm/components/AIConversation/context/ResponseComponentsContext.mjs +6 -2
- package/dist/esm/components/AIConversation/context/elements/IconElement.mjs +2 -2
- package/dist/esm/components/AIConversation/context/elements/definitions.mjs +12 -12
- package/dist/esm/components/AIConversation/createAIConversation.mjs +2 -5
- package/dist/esm/components/AIConversation/displayText.mjs +6 -0
- package/dist/esm/components/AIConversation/utils.mjs +42 -13
- package/dist/esm/components/AIConversation/views/Controls/ActionsBarControl.mjs +3 -2
- package/dist/esm/components/AIConversation/views/Controls/AttachFileControl.mjs +2 -0
- package/dist/esm/components/AIConversation/views/Controls/AttachmentListControl.mjs +2 -0
- package/dist/esm/components/AIConversation/views/Controls/AvatarControl.mjs +2 -0
- package/dist/esm/components/AIConversation/views/Controls/DefaultMessageControl.mjs +2 -0
- package/dist/esm/components/AIConversation/views/Controls/FormControl.mjs +44 -8
- package/dist/esm/components/AIConversation/views/Controls/MessagesControl.mjs +24 -31
- package/dist/esm/components/AIConversation/views/Controls/PromptControl.mjs +2 -0
- package/dist/esm/components/AIConversation/views/default/Form.mjs +13 -20
- package/dist/esm/components/AIConversation/views/default/MessageList.mjs +31 -16
- package/dist/esm/hooks/contentFromEvents.mjs +22 -0
- package/dist/esm/hooks/createAIHooks.mjs +0 -3
- package/dist/esm/hooks/exhaustivelyListMessages.mjs +19 -0
- package/dist/esm/hooks/shared.mjs +14 -0
- package/dist/esm/hooks/useAIConversation.mjs +246 -106
- package/dist/esm/hooks/useAIGeneration.mjs +1 -8
- package/dist/esm/index.mjs +0 -1
- package/dist/esm/version.mjs +1 -1
- package/dist/index.js +508 -280
- package/dist/types/components/AIConversation/AIConversation.d.ts +0 -3
- package/dist/types/components/AIConversation/AIConversationProvider.d.ts +1 -1
- package/dist/types/components/AIConversation/context/AIContextContext.d.ts +6 -0
- package/dist/types/components/AIConversation/context/AttachmentContext.d.ts +5 -5
- package/dist/types/components/AIConversation/context/ControlsContext.d.ts +5 -3
- package/dist/types/components/AIConversation/context/ConversationInputContext.d.ts +4 -2
- package/dist/types/components/AIConversation/context/DisplayTextContext.d.ts +1 -1
- package/dist/types/components/AIConversation/context/FallbackComponentContext.d.ts +7 -0
- package/dist/types/components/AIConversation/context/MessageRenderContext.d.ts +1 -1
- package/dist/types/components/AIConversation/context/ResponseComponentsContext.d.ts +2 -2
- package/dist/types/components/AIConversation/context/elements/IconElement.d.ts +2 -2
- package/dist/types/components/AIConversation/context/elements/definitions.d.ts +12 -12
- package/dist/types/components/AIConversation/context/index.d.ts +4 -2
- package/dist/types/components/AIConversation/createAIConversation.d.ts +0 -3
- package/dist/types/components/AIConversation/displayText.d.ts +2 -0
- package/dist/types/components/AIConversation/index.d.ts +2 -1
- package/dist/types/components/AIConversation/types.d.ts +6 -24
- package/dist/types/components/AIConversation/utils.d.ts +10 -0
- package/dist/types/components/AIConversation/views/Controls/MessagesControl.d.ts +1 -5
- package/dist/types/components/AIConversation/views/default/Attachments.d.ts +2 -2
- package/dist/types/components/AIConversation/views/default/Form.d.ts +1 -1
- package/dist/types/components/AIConversation/views/default/MessageList.d.ts +1 -1
- package/dist/types/components/AIConversation/views/default/PromptList.d.ts +1 -1
- package/dist/types/hooks/contentFromEvents.d.ts +2 -0
- package/dist/types/hooks/createAIHooks.d.ts +0 -3
- package/dist/types/hooks/exhaustivelyListMessages.d.ts +8 -0
- package/dist/types/hooks/index.d.ts +1 -2
- package/dist/types/hooks/shared.d.ts +23 -0
- package/dist/types/hooks/useAIConversation.d.ts +6 -4
- package/dist/types/hooks/useAIGeneration.d.ts +3 -13
- package/dist/types/index.d.ts +1 -1
- package/dist/types/types.d.ts +32 -1
- package/dist/types/version.d.ts +1 -1
- package/package.json +6 -6
- package/dist/ai-conversation-styles.css +0 -195
- package/dist/ai-conversation-styles.js +0 -2
- package/dist/esm/hooks/AIContextProvider.mjs +0 -20
- package/dist/types/ai-conversation-styles.d.ts +0 -1
- package/dist/types/hooks/AIContextProvider.d.ts +0 -17
package/dist/index.js
CHANGED
|
@@ -5,9 +5,9 @@ Object.defineProperty(exports, '__esModule', { value: true });
|
|
|
5
5
|
var React = require('react');
|
|
6
6
|
var elements = require('@aws-amplify/ui-react-core/elements');
|
|
7
7
|
var uiReactCore = require('@aws-amplify/ui-react-core');
|
|
8
|
+
var ui = require('@aws-amplify/ui');
|
|
8
9
|
var uiReact = require('@aws-amplify/ui-react');
|
|
9
10
|
var internal = require('@aws-amplify/ui-react/internal');
|
|
10
|
-
var ui = require('@aws-amplify/ui');
|
|
11
11
|
|
|
12
12
|
function _interopNamespace(e) {
|
|
13
13
|
if (e && e.__esModule) return e;
|
|
@@ -29,6 +29,11 @@ function _interopNamespace(e) {
|
|
|
29
29
|
|
|
30
30
|
var React__namespace = /*#__PURE__*/_interopNamespace(React);
|
|
31
31
|
|
|
32
|
+
const AIContextContext = React__namespace["default"].createContext(undefined);
|
|
33
|
+
const AIContextProvider = ({ children, aiContext, }) => {
|
|
34
|
+
return (React__namespace["default"].createElement(AIContextContext.Provider, { value: aiContext }, children));
|
|
35
|
+
};
|
|
36
|
+
|
|
32
37
|
const ActionsContext = React__namespace["default"].createContext(undefined);
|
|
33
38
|
const ActionsProvider = ({ children, actions, }) => {
|
|
34
39
|
return (React__namespace["default"].createElement(ActionsContext.Provider, { value: actions }, children));
|
|
@@ -42,7 +47,8 @@ const AvatarsProvider = ({ children, avatars, }) => {
|
|
|
42
47
|
const ConversationInputContext = React__namespace["default"].createContext({});
|
|
43
48
|
const ConversationInputContextProvider = ({ children, }) => {
|
|
44
49
|
const [input, setInput] = React__namespace["default"].useState();
|
|
45
|
-
const
|
|
50
|
+
const [error, setError] = React__namespace["default"].useState();
|
|
51
|
+
const providerValue = React__namespace["default"].useMemo(() => ({ input, setInput, error, setError }), [input, setInput, error, setError]);
|
|
46
52
|
return (React__namespace["default"].createElement(ConversationInputContext.Provider, { value: providerValue }, children));
|
|
47
53
|
};
|
|
48
54
|
|
|
@@ -77,32 +83,67 @@ function formatDate(date) {
|
|
|
77
83
|
return `${dateString} at ${timeString}`;
|
|
78
84
|
}
|
|
79
85
|
function arrayBufferToBase64(buffer) {
|
|
80
|
-
let binary = '';
|
|
81
|
-
const bytes = new Uint8Array(buffer);
|
|
82
|
-
const len = bytes.byteLength;
|
|
83
|
-
for (let i = 0; i < len; i++) {
|
|
84
|
-
binary += String.fromCharCode(bytes[i]);
|
|
85
|
-
}
|
|
86
|
-
return window.btoa(binary);
|
|
87
|
-
}
|
|
88
|
-
function convertBufferToBase64(buffer, format) {
|
|
89
|
-
let base64string = '';
|
|
90
86
|
// Use node-based buffer if available
|
|
91
87
|
// fall back on browser if not
|
|
92
88
|
if (typeof Buffer !== 'undefined') {
|
|
93
|
-
|
|
89
|
+
return Buffer.from(new Uint8Array(buffer)).toString('base64');
|
|
94
90
|
}
|
|
95
91
|
else {
|
|
96
|
-
|
|
92
|
+
let binary = '';
|
|
93
|
+
const bytes = new Uint8Array(buffer);
|
|
94
|
+
const len = bytes.byteLength;
|
|
95
|
+
for (let i = 0; i < len; i++) {
|
|
96
|
+
binary += String.fromCharCode(bytes[i]);
|
|
97
|
+
}
|
|
98
|
+
return window.btoa(binary);
|
|
97
99
|
}
|
|
100
|
+
}
|
|
101
|
+
function convertBufferToBase64(buffer, format) {
|
|
102
|
+
const base64string = arrayBufferToBase64(buffer);
|
|
98
103
|
return `data:image/${format};base64,${base64string}`;
|
|
99
104
|
}
|
|
100
105
|
function getImageTypeFromMimeType(mimeType) {
|
|
101
106
|
return mimeType.split('/')[1];
|
|
102
107
|
}
|
|
108
|
+
async function attachmentsValidator({ files, maxAttachments, maxAttachmentSize, }) {
|
|
109
|
+
const acceptedFiles = [];
|
|
110
|
+
const rejectedFiles = [];
|
|
111
|
+
let hasMaxSizeError = false;
|
|
112
|
+
for (const file of files) {
|
|
113
|
+
const arrayBuffer = await file.arrayBuffer();
|
|
114
|
+
const base64 = arrayBufferToBase64(arrayBuffer);
|
|
115
|
+
if (base64.length < maxAttachmentSize) {
|
|
116
|
+
acceptedFiles.push(file);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
rejectedFiles.push(file);
|
|
120
|
+
hasMaxSizeError = true;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (acceptedFiles.length > maxAttachments) {
|
|
124
|
+
return {
|
|
125
|
+
acceptedFiles: acceptedFiles.slice(0, maxAttachments),
|
|
126
|
+
rejectedFiles: [...acceptedFiles.slice(maxAttachments), ...rejectedFiles],
|
|
127
|
+
hasMaxAttachmentsError: true,
|
|
128
|
+
hasMaxAttachmentSizeError: hasMaxSizeError,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
acceptedFiles,
|
|
133
|
+
rejectedFiles,
|
|
134
|
+
hasMaxAttachmentsError: false,
|
|
135
|
+
hasMaxAttachmentSizeError: hasMaxSizeError,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
103
138
|
|
|
104
139
|
const defaultAIConversationDisplayTextEn = {
|
|
105
140
|
getMessageTimestampText: (date) => formatDate(date),
|
|
141
|
+
getMaxAttachmentErrorText(count) {
|
|
142
|
+
return `Cannot choose more than ${count} ${count === 1 ? 'file' : 'files'}. `;
|
|
143
|
+
},
|
|
144
|
+
getAttachmentSizeErrorText(sizeText) {
|
|
145
|
+
return `File size must be below ${sizeText}.`;
|
|
146
|
+
},
|
|
106
147
|
};
|
|
107
148
|
|
|
108
149
|
const { ConversationDisplayTextContext, ConversationDisplayTextProvider, useConversationDisplayText, } = uiReactCore.createContextUtilities({
|
|
@@ -141,8 +182,12 @@ const convertResponseComponentsToToolConfiguration = (responseComponents) => {
|
|
|
141
182
|
const { props } = responseComponents[toolName];
|
|
142
183
|
const requiredProps = [];
|
|
143
184
|
Object.keys(props).forEach((propName) => {
|
|
144
|
-
if (props[propName].required)
|
|
185
|
+
if (props[propName].required) {
|
|
145
186
|
requiredProps.push(propName);
|
|
187
|
+
// The inputSchema for a tool needs to not
|
|
188
|
+
// have `required` in the properties
|
|
189
|
+
props[propName].required = undefined;
|
|
190
|
+
}
|
|
146
191
|
});
|
|
147
192
|
tools[toolName] = {
|
|
148
193
|
description: responseComponents[toolName].description,
|
|
@@ -171,9 +216,18 @@ const { MessageRendererContext, MessageRendererProvider, useMessageRenderer, } =
|
|
|
171
216
|
errorMessage: '`useMessageRenderer` must be used with an AIConversation component',
|
|
172
217
|
});
|
|
173
218
|
|
|
174
|
-
const AttachmentContext = React__namespace.createContext(
|
|
175
|
-
|
|
176
|
-
|
|
219
|
+
const AttachmentContext = React__namespace.createContext({
|
|
220
|
+
allowAttachments: false,
|
|
221
|
+
// We save attachments as base64 strings into dynamodb for conversation history
|
|
222
|
+
// DynamoDB has a max size of 400kb for records
|
|
223
|
+
// This can be overridden so cutsomers could provide a lower number
|
|
224
|
+
// or a higher number if in the future we support larger sizes.
|
|
225
|
+
maxAttachmentSize: 400000,
|
|
226
|
+
maxAttachments: 20,
|
|
227
|
+
});
|
|
228
|
+
const AttachmentProvider = ({ children, allowAttachments = false, maxAttachmentSize = 400000, maxAttachments = 20, }) => {
|
|
229
|
+
const providerValue = React__namespace.useMemo(() => ({ maxAttachmentSize, maxAttachments, allowAttachments }), [maxAttachmentSize, maxAttachments, allowAttachments]);
|
|
230
|
+
return (React__namespace.createElement(AttachmentContext.Provider, { value: providerValue }, children));
|
|
177
231
|
};
|
|
178
232
|
|
|
179
233
|
const WelcomeMessageContext = React__namespace.createContext(undefined);
|
|
@@ -181,6 +235,11 @@ const WelcomeMessageProvider = ({ children, welcomeMessage, }) => {
|
|
|
181
235
|
return (React__namespace.createElement(WelcomeMessageContext.Provider, { value: welcomeMessage }, children));
|
|
182
236
|
};
|
|
183
237
|
|
|
238
|
+
const FallbackComponentContext = React__namespace["default"].createContext(undefined);
|
|
239
|
+
const FallbackComponentProvider = ({ children, FallbackComponent, }) => {
|
|
240
|
+
return (React__namespace["default"].createElement(FallbackComponentContext.Provider, { value: FallbackComponent }, children));
|
|
241
|
+
};
|
|
242
|
+
|
|
184
243
|
const DEFAULT_ICON_PATHS = {
|
|
185
244
|
attach: 'M720-330q0 104-73 177T470-80q-104 0-177-73t-73-177v-370q0-75 52.5-127.5T400-880q75 0 127.5 52.5T580-700v350q0 46-32 78t-78 32q-46 0-78-32t-32-78v-370h80v370q0 13 8.5 21.5T470-320q13 0 21.5-8.5T500-350v-350q-1-42-29.5-71T400-800q-42 0-71 29t-29 71v370q-1 71 49 120.5T470-160q70 0 119-49.5T640-330v-390h80v390Z',
|
|
186
245
|
close: 'm256-200-56-56 224-224-224-224 56-56 224 224 224-224 56 56-224 224 224 224-56 56-224-224-224 224Z',
|
|
@@ -197,7 +256,7 @@ const DEFAULT_ICON_ATTRIBUTES = {
|
|
|
197
256
|
fill: 'none',
|
|
198
257
|
xmlns: 'http://www.w3.org/2000/svg',
|
|
199
258
|
};
|
|
200
|
-
const BaseIconElement = elements.
|
|
259
|
+
const BaseIconElement = elements.defineBaseElementWithRef({
|
|
201
260
|
type: 'svg',
|
|
202
261
|
displayName: 'Icon',
|
|
203
262
|
});
|
|
@@ -213,44 +272,44 @@ const getIconProps = ({ variant, ...props }) => {
|
|
|
213
272
|
};
|
|
214
273
|
const IconElement = elements.withBaseElementProps(BaseIconElement, getIconProps);
|
|
215
274
|
|
|
216
|
-
const LabelElement$1 = elements.
|
|
275
|
+
const LabelElement$1 = elements.defineBaseElementWithRef({
|
|
217
276
|
type: 'label',
|
|
218
277
|
displayName: 'Label',
|
|
219
278
|
});
|
|
220
|
-
const TextElement = elements.
|
|
279
|
+
const TextElement = elements.defineBaseElementWithRef({
|
|
221
280
|
type: 'p',
|
|
222
281
|
displayName: 'Text',
|
|
223
282
|
});
|
|
224
|
-
const UnorderedListElement = elements.
|
|
283
|
+
const UnorderedListElement = elements.defineBaseElementWithRef({
|
|
225
284
|
type: 'ul',
|
|
226
285
|
displayName: 'UnorderedList',
|
|
227
286
|
});
|
|
228
|
-
const ListItemElement = elements.
|
|
287
|
+
const ListItemElement = elements.defineBaseElementWithRef({
|
|
229
288
|
type: 'li',
|
|
230
289
|
displayName: 'ListItem',
|
|
231
290
|
});
|
|
232
|
-
const HeadingElement = elements.
|
|
291
|
+
const HeadingElement = elements.defineBaseElementWithRef({
|
|
233
292
|
type: 'h2',
|
|
234
293
|
displayName: 'Title',
|
|
235
294
|
});
|
|
236
|
-
const ImageElement = elements.
|
|
295
|
+
const ImageElement = elements.defineBaseElementWithRef({
|
|
237
296
|
type: 'img',
|
|
238
297
|
displayName: 'Image',
|
|
239
298
|
});
|
|
240
|
-
const InputElement = elements.
|
|
299
|
+
const InputElement = elements.defineBaseElementWithRef({
|
|
241
300
|
type: 'input',
|
|
242
301
|
displayName: 'Input',
|
|
243
302
|
});
|
|
244
|
-
const ButtonElement = elements.
|
|
245
|
-
const ViewElement = elements.
|
|
303
|
+
const ButtonElement = elements.defineBaseElementWithRef({ type: 'button', displayName: 'Button' });
|
|
304
|
+
const ViewElement = elements.defineBaseElementWithRef({
|
|
246
305
|
type: 'div',
|
|
247
306
|
displayName: 'View',
|
|
248
307
|
});
|
|
249
|
-
const SpanElement = elements.
|
|
308
|
+
const SpanElement = elements.defineBaseElementWithRef({
|
|
250
309
|
type: 'span',
|
|
251
310
|
displayName: 'Span',
|
|
252
311
|
});
|
|
253
|
-
const TextAreaElement = elements.
|
|
312
|
+
const TextAreaElement = elements.defineBaseElementWithRef({
|
|
254
313
|
type: 'textarea',
|
|
255
314
|
displayName: 'TextArea',
|
|
256
315
|
});
|
|
@@ -269,9 +328,9 @@ const AIConversationElements = {
|
|
|
269
328
|
View: ViewElement,
|
|
270
329
|
};
|
|
271
330
|
|
|
272
|
-
const { Button: Button$4, Span: Span$
|
|
331
|
+
const { Button: Button$4, Span: Span$2, View: View$6 } = AIConversationElements;
|
|
273
332
|
const ACTIONS_BAR_BLOCK = 'ai-actions-bar';
|
|
274
|
-
const ActionIcon = elements.withBaseElementProps(Span$
|
|
333
|
+
const ActionIcon = elements.withBaseElementProps(Span$2, {
|
|
275
334
|
'aria-hidden': 'true',
|
|
276
335
|
className: `${ACTIONS_BAR_BLOCK}__icon`,
|
|
277
336
|
});
|
|
@@ -286,14 +345,13 @@ const Container$3 = elements.withBaseElementProps(View$6, {
|
|
|
286
345
|
});
|
|
287
346
|
const ActionsBarControl = ({ message, focusable, }) => {
|
|
288
347
|
const actions = React__namespace["default"].useContext(ActionsContext);
|
|
289
|
-
return (React__namespace["default"].createElement(Container$3, null, actions?.map((action, index) => (React__namespace["default"].createElement(ActionButton, {
|
|
290
|
-
React__namespace["default"].createElement(ActionIcon, { "data-testid": `action-icon-${action.displayName}` }, action.icon))))));
|
|
348
|
+
return (React__namespace["default"].createElement(Container$3, null, actions?.map((action, index) => (React__namespace["default"].createElement(ActionButton, { key: index, onClick: () => action.handler(message), tabIndex: focusable ? 0 : -1 }, action.component)))));
|
|
291
349
|
};
|
|
292
350
|
ActionsBarControl.Button = ActionButton;
|
|
293
351
|
ActionsBarControl.Container = Container$3;
|
|
294
352
|
ActionsBarControl.Icon = ActionIcon;
|
|
295
353
|
|
|
296
|
-
const { Icon: Icon$3, Span: Span$
|
|
354
|
+
const { Icon: Icon$3, Span: Span$1, Text: Text$2, View: View$5 } = AIConversationElements;
|
|
297
355
|
const AVATAR_BLOCK = 'ai-avatar';
|
|
298
356
|
const DEFAULT_USER_ICON = elements.withBaseElementProps(Icon$3, {
|
|
299
357
|
variant: 'user-avatar',
|
|
@@ -304,7 +362,7 @@ const DEFAULT_AI_ICON = () => (React__namespace["default"].createElement("svg",
|
|
|
304
362
|
const AvatarDisplayName = elements.withBaseElementProps(Text$2, {
|
|
305
363
|
className: `${AVATAR_BLOCK}__display-name`,
|
|
306
364
|
});
|
|
307
|
-
const AvatarIcon = elements.withBaseElementProps(Span$
|
|
365
|
+
const AvatarIcon = elements.withBaseElementProps(Span$1, {
|
|
308
366
|
'aria-hidden': true,
|
|
309
367
|
className: `${AVATAR_BLOCK}__icon`,
|
|
310
368
|
});
|
|
@@ -385,7 +443,7 @@ AttachFileControl.Icon = AttachFileIcon;
|
|
|
385
443
|
AttachFileControl.Button = AttachFileButton;
|
|
386
444
|
AttachFileControl.Container = AttachFileContainer;
|
|
387
445
|
|
|
388
|
-
const { Button: Button$2, Icon: Icon$1, ListItem, UnorderedList: ListElement, Span
|
|
446
|
+
const { Button: Button$2, Icon: Icon$1, ListItem, UnorderedList: ListElement, Span, Text: Text$1, View: View$3, } = AIConversationElements;
|
|
389
447
|
const IMAGE_LIST_BLOCK = 'ai-attachment-list';
|
|
390
448
|
const IMAGE_ITEM_BLOCK = 'ai-attachment';
|
|
391
449
|
const REMOVE_IMAGE_BLOCK = 'ai-remove-attachment';
|
|
@@ -416,7 +474,7 @@ const FileNameText = elements.withBaseElementProps(Text$1, {
|
|
|
416
474
|
const FileSizeText = elements.withBaseElementProps(Text$1, {
|
|
417
475
|
className: `${IMAGE_TEXT_BLOCK}__file-size`,
|
|
418
476
|
});
|
|
419
|
-
const Separator
|
|
477
|
+
const Separator = elements.withBaseElementProps(Span, {
|
|
420
478
|
'aria-hidden': true,
|
|
421
479
|
className: `${IMAGE_TEXT_BLOCK}__separator`,
|
|
422
480
|
children: '|',
|
|
@@ -427,13 +485,13 @@ const TextContainer = elements.withBaseElementProps(View$3, {
|
|
|
427
485
|
const TextControl = ({ fileName, fileSize }) => {
|
|
428
486
|
return (React__namespace["default"].createElement(TextContainer, null,
|
|
429
487
|
React__namespace["default"].createElement(FileNameText, null, fileName),
|
|
430
|
-
React__namespace["default"].createElement(Separator
|
|
488
|
+
React__namespace["default"].createElement(Separator, null),
|
|
431
489
|
React__namespace["default"].createElement(FileSizeText, null, fileSize)));
|
|
432
490
|
};
|
|
433
491
|
TextControl.Container = TextContainer;
|
|
434
492
|
TextControl.FileName = FileNameText;
|
|
435
493
|
TextControl.FileSize = FileSizeText;
|
|
436
|
-
TextControl.Separator = Separator
|
|
494
|
+
TextControl.Separator = Separator;
|
|
437
495
|
const Container$1 = elements.withBaseElementProps(ListItem, {
|
|
438
496
|
className: `${IMAGE_ITEM_BLOCK}__list-item`,
|
|
439
497
|
});
|
|
@@ -548,12 +606,16 @@ const InputContainer = elements.withBaseElementProps(View$2, {
|
|
|
548
606
|
className: `${FIELD_BLOCK}__input-container`,
|
|
549
607
|
});
|
|
550
608
|
const FormControl = () => {
|
|
551
|
-
const { input, setInput } = React__namespace["default"].useContext(ConversationInputContext);
|
|
609
|
+
const { input, setInput, error, setError } = React__namespace["default"].useContext(ConversationInputContext);
|
|
552
610
|
const handleSendMessage = React__namespace["default"].useContext(SendMessageContext);
|
|
553
|
-
const allowAttachments = React__namespace["default"].useContext(AttachmentContext);
|
|
554
|
-
const
|
|
611
|
+
const { allowAttachments, maxAttachmentSize, maxAttachments } = React__namespace["default"].useContext(AttachmentContext);
|
|
612
|
+
const displayText = useConversationDisplayText();
|
|
555
613
|
const responseComponents = React__namespace["default"].useContext(ResponseComponentsContext);
|
|
614
|
+
const isLoading = React__namespace["default"].useContext(LoadingContext);
|
|
615
|
+
const aiContext = React__namespace["default"].useContext(AIContextContext);
|
|
616
|
+
const ref = React__namespace["default"].useRef(null);
|
|
556
617
|
const controls = React__namespace["default"].useContext(ControlsContext);
|
|
618
|
+
const [composing, setComposing] = React__namespace["default"].useState(false);
|
|
557
619
|
const submitMessage = async () => {
|
|
558
620
|
ref.current?.reset();
|
|
559
621
|
const submittedContent = [];
|
|
@@ -578,6 +640,7 @@ const FormControl = () => {
|
|
|
578
640
|
if (handleSendMessage) {
|
|
579
641
|
handleSendMessage({
|
|
580
642
|
content: submittedContent,
|
|
643
|
+
aiContext: ui.isFunction(aiContext) ? aiContext() : undefined,
|
|
581
644
|
toolConfiguration: convertResponseComponentsToToolConfiguration(responseComponents),
|
|
582
645
|
});
|
|
583
646
|
}
|
|
@@ -590,7 +653,7 @@ const FormControl = () => {
|
|
|
590
653
|
};
|
|
591
654
|
const handleOnKeyDown = (event) => {
|
|
592
655
|
const { key, shiftKey } = event;
|
|
593
|
-
if (key === 'Enter' && !shiftKey) {
|
|
656
|
+
if (key === 'Enter' && !shiftKey && !composing) {
|
|
594
657
|
event.preventDefault();
|
|
595
658
|
const hasInput = !!input?.text || (input?.files?.length && input?.files?.length > 0);
|
|
596
659
|
if (hasInput) {
|
|
@@ -598,15 +661,43 @@ const FormControl = () => {
|
|
|
598
661
|
}
|
|
599
662
|
}
|
|
600
663
|
};
|
|
664
|
+
const onValidate = React__namespace["default"].useCallback(async (files) => {
|
|
665
|
+
const previousFiles = input?.files ?? [];
|
|
666
|
+
const { acceptedFiles, hasMaxAttachmentsError, hasMaxAttachmentSizeError, } = await attachmentsValidator({
|
|
667
|
+
files: [...files, ...previousFiles],
|
|
668
|
+
maxAttachments,
|
|
669
|
+
maxAttachmentSize,
|
|
670
|
+
});
|
|
671
|
+
if (hasMaxAttachmentsError || hasMaxAttachmentSizeError) {
|
|
672
|
+
const errors = [];
|
|
673
|
+
if (hasMaxAttachmentsError) {
|
|
674
|
+
errors.push(displayText.getMaxAttachmentErrorText(maxAttachments));
|
|
675
|
+
}
|
|
676
|
+
if (hasMaxAttachmentSizeError) {
|
|
677
|
+
errors.push(displayText.getAttachmentSizeErrorText(
|
|
678
|
+
// base64 size is about 137% that of the file size
|
|
679
|
+
// https://en.wikipedia.org/wiki/Base64#MIME
|
|
680
|
+
ui.humanFileSize((maxAttachmentSize - 814) / 1.37, true)));
|
|
681
|
+
}
|
|
682
|
+
setError?.(errors.join(' '));
|
|
683
|
+
}
|
|
684
|
+
else {
|
|
685
|
+
setError?.(undefined);
|
|
686
|
+
}
|
|
687
|
+
setInput?.((prevValue) => ({
|
|
688
|
+
...prevValue,
|
|
689
|
+
files: acceptedFiles,
|
|
690
|
+
}));
|
|
691
|
+
}, [setInput, input, displayText, maxAttachmentSize, maxAttachments, setError]);
|
|
601
692
|
if (controls?.Form) {
|
|
602
|
-
return (React__namespace["default"].createElement(controls.Form, { handleSubmit: handleSubmit, input: input, setInput: setInput, allowAttachments: allowAttachments }));
|
|
693
|
+
return (React__namespace["default"].createElement(controls.Form, { handleSubmit: handleSubmit, input: input, setInput: setInput, onValidate: onValidate, allowAttachments: allowAttachments, isLoading: isLoading, error: error, setError: setError }));
|
|
603
694
|
}
|
|
604
695
|
return (React__namespace["default"].createElement("form", { className: `${FIELD_BLOCK}__form`, onSubmit: handleSubmit, method: "post", ref: ref },
|
|
605
696
|
allowAttachments ? React__namespace["default"].createElement(AttachFileControl, null) : null,
|
|
606
697
|
React__namespace["default"].createElement(InputContainer, null,
|
|
607
698
|
React__namespace["default"].createElement(VisuallyHidden, null,
|
|
608
699
|
React__namespace["default"].createElement(Label, null)),
|
|
609
|
-
React__namespace["default"].createElement(TextInput, { onKeyDown: handleOnKeyDown }),
|
|
700
|
+
React__namespace["default"].createElement(TextInput, { onKeyDown: handleOnKeyDown, onCompositionStart: () => setComposing(true), onCompositionEnd: () => setComposing(false) }),
|
|
610
701
|
React__namespace["default"].createElement(AttachmentListControl, null)),
|
|
611
702
|
React__namespace["default"].createElement(SendButton, null,
|
|
612
703
|
React__namespace["default"].createElement(SendIcon, null))));
|
|
@@ -618,61 +709,52 @@ FormControl.TextInput = TextInput;
|
|
|
618
709
|
FormControl.SendButton = SendButton;
|
|
619
710
|
FormControl.SendIcon = SendIcon;
|
|
620
711
|
|
|
621
|
-
const {
|
|
622
|
-
const MESSAGES_BLOCK = 'ai-
|
|
623
|
-
const MESSAGE_BLOCK = 'ai-
|
|
624
|
-
const
|
|
625
|
-
alt: 'Image attachment',
|
|
626
|
-
});
|
|
627
|
-
const MediaContent = React__namespace["default"].forwardRef(function MediaContent(props, ref) {
|
|
712
|
+
const { Text, View: View$1 } = AIConversationElements;
|
|
713
|
+
const MESSAGES_BLOCK = 'amplify-ai-conversation__message__list';
|
|
714
|
+
const MESSAGE_BLOCK = 'amplify-ai-conversation__message';
|
|
715
|
+
const MediaContent = (props) => {
|
|
628
716
|
const variant = React__namespace["default"].useContext(MessageVariantContext);
|
|
629
717
|
const role = React__namespace["default"].useContext(RoleContext);
|
|
630
|
-
return (React__namespace["default"].createElement(
|
|
631
|
-
}
|
|
718
|
+
return (React__namespace["default"].createElement(uiReact.Image, { className: ui.classNames(`${MESSAGE_BLOCK}__image`, variant && `${MESSAGE_BLOCK}__image--${variant}`, `${MESSAGE_BLOCK}__image--${role}`), ...props }));
|
|
719
|
+
};
|
|
632
720
|
const TextContent = React__namespace["default"].forwardRef(function TextContent(props, ref) {
|
|
633
721
|
return React__namespace["default"].createElement(Text, { ref: ref, className: `${MESSAGE_BLOCK}__text`, ...props });
|
|
634
722
|
});
|
|
635
|
-
const ContentContainer = React__namespace["default"].forwardRef(function ContentContainer(props, ref) {
|
|
636
|
-
const variant = React__namespace["default"].useContext(MessageVariantContext);
|
|
637
|
-
return (React__namespace["default"].createElement(View$1, { "data-testid": 'content', className: `${MESSAGE_BLOCK}__content ${MESSAGE_BLOCK}__content--${variant}`, ref: ref, ...props }));
|
|
638
|
-
});
|
|
639
723
|
const ToolContent = ({ toolUse, }) => {
|
|
640
|
-
const responseComponents = React__namespace["default"].useContext(ResponseComponentsContext);
|
|
724
|
+
const responseComponents = React__namespace["default"].useContext(ResponseComponentsContext) ?? {};
|
|
725
|
+
const FallbackComponent = React__namespace["default"].useContext(FallbackComponentContext);
|
|
641
726
|
// For now tool use is limited to custom response components
|
|
642
727
|
const { name, input } = toolUse;
|
|
643
|
-
if (!
|
|
644
|
-
!name ||
|
|
645
|
-
!name.startsWith(RESPONSE_COMPONENT_PREFIX)) {
|
|
728
|
+
if (!name || !name.startsWith(RESPONSE_COMPONENT_PREFIX)) {
|
|
646
729
|
return;
|
|
647
730
|
}
|
|
648
731
|
else {
|
|
649
732
|
const response = responseComponents[name];
|
|
650
|
-
|
|
651
|
-
|
|
733
|
+
if (response) {
|
|
734
|
+
const CustomComponent = response.component;
|
|
735
|
+
return React__namespace["default"].createElement(CustomComponent, { ...input });
|
|
736
|
+
}
|
|
737
|
+
// fallback if there is a UI component message but we don't have
|
|
738
|
+
// a React component that matches
|
|
739
|
+
if (FallbackComponent) {
|
|
740
|
+
return React__namespace["default"].createElement(FallbackComponent, { ...input });
|
|
741
|
+
}
|
|
652
742
|
}
|
|
653
743
|
};
|
|
654
744
|
const MessageControl = ({ message }) => {
|
|
655
745
|
const messageRenderer = React__namespace["default"].useContext(MessageRendererContext);
|
|
656
|
-
return (React__namespace["default"].createElement(
|
|
746
|
+
return (React__namespace["default"].createElement(React__namespace["default"].Fragment, null, message.content.map((content, index) => {
|
|
657
747
|
if (content.text) {
|
|
658
748
|
return messageRenderer?.text ? (React__namespace["default"].createElement(React__namespace["default"].Fragment, { key: index }, messageRenderer.text({ text: content.text }))) : (React__namespace["default"].createElement(TextContent, { "data-testid": 'text-content', key: index }, content.text));
|
|
659
749
|
}
|
|
660
750
|
else if (content.image) {
|
|
661
|
-
return messageRenderer?.image ? (React__namespace["default"].createElement(React__namespace["default"].Fragment, { key: index }, messageRenderer?.image({ image: content.image }))) : (React__namespace["default"].createElement(MediaContent, { "data-testid": 'image-content', key: index, src: convertBufferToBase64(content.image?.source.bytes, content.image?.format) }));
|
|
751
|
+
return messageRenderer?.image ? (React__namespace["default"].createElement(React__namespace["default"].Fragment, { key: index }, messageRenderer?.image({ image: content.image }))) : (React__namespace["default"].createElement(MediaContent, { "data-testid": 'image-content', key: index, alt: "", src: convertBufferToBase64(content.image?.source.bytes, content.image?.format) }));
|
|
662
752
|
}
|
|
663
753
|
else if (content.toolUse) {
|
|
664
754
|
return React__namespace["default"].createElement(ToolContent, { toolUse: content.toolUse, key: index });
|
|
665
755
|
}
|
|
666
756
|
})));
|
|
667
757
|
};
|
|
668
|
-
MessageControl.Container = ContentContainer;
|
|
669
|
-
MessageControl.MediaContent = MediaContent;
|
|
670
|
-
MessageControl.TextContent = TextContent;
|
|
671
|
-
const Separator = elements.withBaseElementProps(Span, {
|
|
672
|
-
'aria-hidden': true,
|
|
673
|
-
children: '|',
|
|
674
|
-
className: `${MESSAGE_BLOCK}__separator`,
|
|
675
|
-
});
|
|
676
758
|
const Timestamp = elements.withBaseElementProps(Text, {
|
|
677
759
|
className: `${MESSAGE_BLOCK}__timestamp`,
|
|
678
760
|
});
|
|
@@ -733,7 +815,6 @@ const MessagesControl = () => {
|
|
|
733
815
|
React__namespace["default"].createElement(MessageContainer, { "data-testid": `message`, key: `message-${index}`, tabIndex: focusedItemIndex === index ? 0 : -1, onFocus: () => handleFocus(index), onKeyDown: (event) => onKeyDown(index, event), ref: (el) => (messagesRef.current[index] = el) },
|
|
734
816
|
React__namespace["default"].createElement(HeaderContainer, null,
|
|
735
817
|
React__namespace["default"].createElement(AvatarControl, null),
|
|
736
|
-
React__namespace["default"].createElement(Separator, null),
|
|
737
818
|
React__namespace["default"].createElement(Timestamp, null, getMessageTimestampText(new Date(message.createdAt)))),
|
|
738
819
|
React__namespace["default"].createElement(MessageControl, { message: message }),
|
|
739
820
|
message.role === 'assistant' ? (React__namespace["default"].createElement(ActionsBarControl, { message: message, focusable: focusedItemIndex === index })) : null)));
|
|
@@ -745,7 +826,6 @@ MessagesControl.Container = MessageContainer;
|
|
|
745
826
|
MessagesControl.HeaderContainer = HeaderContainer;
|
|
746
827
|
MessagesControl.Layout = Layout;
|
|
747
828
|
MessagesControl.Message = MessageControl;
|
|
748
|
-
MessagesControl.Separator = Separator;
|
|
749
829
|
|
|
750
830
|
const { View, Button } = AIConversationElements;
|
|
751
831
|
const PROMPT_BLOCK = 'ai-prompts';
|
|
@@ -789,25 +869,27 @@ PromptControl.Container = Container;
|
|
|
789
869
|
PromptControl.PromptGroup = PromptGroup;
|
|
790
870
|
PromptControl.PromptCard = PromptCard;
|
|
791
871
|
|
|
792
|
-
const AIConversationProvider = ({ actions, allowAttachments, avatars, children, controls, displayText,
|
|
872
|
+
const AIConversationProvider = ({ aiContext, actions, allowAttachments, avatars, children, controls, displayText, handleSendMessage, isLoading, maxAttachmentSize, maxAttachments, messages, messageRenderer, responseComponents, suggestedPrompts, variant, welcomeMessage, FallbackResponseComponent, }) => {
|
|
793
873
|
const _displayText = {
|
|
794
874
|
...defaultAIConversationDisplayTextEn,
|
|
795
875
|
...displayText,
|
|
796
876
|
};
|
|
797
|
-
return (React__namespace["default"].createElement(
|
|
798
|
-
React__namespace["default"].createElement(
|
|
799
|
-
React__namespace["default"].createElement(
|
|
800
|
-
React__namespace["default"].createElement(
|
|
801
|
-
React__namespace["default"].createElement(
|
|
802
|
-
React__namespace["default"].createElement(
|
|
803
|
-
React__namespace["default"].createElement(
|
|
804
|
-
React__namespace["default"].createElement(
|
|
805
|
-
React__namespace["default"].createElement(
|
|
806
|
-
React__namespace["default"].createElement(
|
|
807
|
-
React__namespace["default"].createElement(
|
|
808
|
-
React__namespace["default"].createElement(
|
|
809
|
-
React__namespace["default"].createElement(
|
|
810
|
-
React__namespace["default"].createElement(
|
|
877
|
+
return (React__namespace["default"].createElement(ControlsProvider, { controls: controls },
|
|
878
|
+
React__namespace["default"].createElement(SuggestedPromptProvider, { suggestedPrompts: suggestedPrompts },
|
|
879
|
+
React__namespace["default"].createElement(WelcomeMessageProvider, { welcomeMessage: welcomeMessage },
|
|
880
|
+
React__namespace["default"].createElement(FallbackComponentProvider, { FallbackComponent: FallbackResponseComponent },
|
|
881
|
+
React__namespace["default"].createElement(MessageRendererProvider, { ...messageRenderer },
|
|
882
|
+
React__namespace["default"].createElement(ResponseComponentsProvider, { responseComponents: responseComponents },
|
|
883
|
+
React__namespace["default"].createElement(AttachmentProvider, { allowAttachments: allowAttachments, maxAttachmentSize: maxAttachmentSize, maxAttachments: maxAttachments },
|
|
884
|
+
React__namespace["default"].createElement(ConversationDisplayTextProvider, { ..._displayText },
|
|
885
|
+
React__namespace["default"].createElement(ConversationInputContextProvider, null,
|
|
886
|
+
React__namespace["default"].createElement(SendMessageContextProvider, { handleSendMessage: handleSendMessage },
|
|
887
|
+
React__namespace["default"].createElement(AvatarsProvider, { avatars: avatars },
|
|
888
|
+
React__namespace["default"].createElement(ActionsProvider, { actions: actions },
|
|
889
|
+
React__namespace["default"].createElement(MessageVariantProvider, { variant: variant },
|
|
890
|
+
React__namespace["default"].createElement(MessagesProvider, { messages: messages },
|
|
891
|
+
React__namespace["default"].createElement(AIContextProvider, { aiContext: aiContext },
|
|
892
|
+
React__namespace["default"].createElement(LoadingContextProvider, { isLoading: isLoading }, children)))))))))))))))));
|
|
811
893
|
};
|
|
812
894
|
|
|
813
895
|
const DefaultMessageControl = () => {
|
|
@@ -820,15 +902,11 @@ const DefaultMessageControl = () => {
|
|
|
820
902
|
}
|
|
821
903
|
};
|
|
822
904
|
|
|
823
|
-
/**
|
|
824
|
-
* @experimental
|
|
825
|
-
*/
|
|
826
905
|
function createAIConversation(input = {}) {
|
|
827
|
-
const {
|
|
906
|
+
const { suggestedPrompts, actions, responseComponents, variant, controls, displayText, allowAttachments, messageRenderer, FallbackResponseComponent, } = input;
|
|
828
907
|
function AIConversation(props) {
|
|
829
908
|
const { messages, avatars, handleSendMessage, isLoading } = props;
|
|
830
909
|
const providerProps = {
|
|
831
|
-
elements,
|
|
832
910
|
actions,
|
|
833
911
|
suggestedPrompts,
|
|
834
912
|
responseComponents,
|
|
@@ -841,6 +919,7 @@ function createAIConversation(input = {}) {
|
|
|
841
919
|
handleSendMessage,
|
|
842
920
|
isLoading,
|
|
843
921
|
messageRenderer,
|
|
922
|
+
FallbackResponseComponent,
|
|
844
923
|
};
|
|
845
924
|
return (React__namespace["default"].createElement(AIConversationProvider, { ...providerProps },
|
|
846
925
|
React__namespace["default"].createElement(ViewElement, null,
|
|
@@ -857,6 +936,16 @@ function createAIConversation(input = {}) {
|
|
|
857
936
|
return { AIConversation };
|
|
858
937
|
}
|
|
859
938
|
|
|
939
|
+
const PlaceholderMessage = ({ role }) => {
|
|
940
|
+
const variant = React__namespace.useContext(MessageVariantContext);
|
|
941
|
+
return (React__namespace.createElement(uiReact.View, { className: ui.classNames(ui.ComponentClassName.AIConversationMessage, ui.classNameModifier(ui.ComponentClassName.AIConversationMessage, variant), ui.classNameModifier(ui.ComponentClassName.AIConversationMessage, role)) },
|
|
942
|
+
React__namespace.createElement(uiReact.View, { className: ui.ComponentClassName.AIConversationMessageAvatar },
|
|
943
|
+
React__namespace.createElement(uiReact.Avatar, null)),
|
|
944
|
+
React__namespace.createElement(uiReact.View, { className: ui.ComponentClassName.AIConversationMessageBody },
|
|
945
|
+
React__namespace.createElement(uiReact.Placeholder, { width: "25%" }),
|
|
946
|
+
React__namespace.createElement(uiReact.Placeholder, { width: "50%" }),
|
|
947
|
+
React__namespace.createElement(uiReact.Placeholder, { width: "25%" }))));
|
|
948
|
+
};
|
|
860
949
|
const MessageMeta = ({ message }) => {
|
|
861
950
|
// need to pass this in as props in order for it to be overridable
|
|
862
951
|
const avatars = React__namespace.useContext(AvatarsContext);
|
|
@@ -868,29 +957,30 @@ const MessageMeta = ({ message }) => {
|
|
|
868
957
|
React__namespace.createElement(uiReact.Text, { className: ui.ComponentClassName.AIConversationMessageSenderUsername }, avatar?.username),
|
|
869
958
|
React__namespace.createElement(uiReact.Text, { className: ui.ComponentClassName.AIConversationMessageSenderTimestamp }, getMessageTimestampText(new Date(message.createdAt)))));
|
|
870
959
|
};
|
|
871
|
-
const
|
|
872
|
-
const
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
return (React__namespace.createElement(uiReact.View, { className: ui.
|
|
876
|
-
React__namespace.createElement(uiReact.
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
React__namespace.createElement(uiReact.Text, { className: ui.ComponentClassName.AIConversationMessageSenderUsername }, avatar?.username)))));
|
|
960
|
+
const MessageActions = ({ message }) => {
|
|
961
|
+
const actions = React__namespace.useContext(ActionsContext);
|
|
962
|
+
if (!actions)
|
|
963
|
+
return null;
|
|
964
|
+
return (React__namespace.createElement(uiReact.View, { className: ui.ComponentClassName.AIConversationMessageActions }, actions.map((action, i) => {
|
|
965
|
+
return (React__namespace.createElement(uiReact.Button, { key: i, size: "small", onClick: () => {
|
|
966
|
+
action.handler(message);
|
|
967
|
+
} }, action.component));
|
|
968
|
+
})));
|
|
881
969
|
};
|
|
882
970
|
const Message = ({ message }) => {
|
|
883
971
|
const avatars = React__namespace.useContext(AvatarsContext);
|
|
884
972
|
const variant = React__namespace.useContext(MessageVariantContext);
|
|
973
|
+
const { isLoading } = message;
|
|
885
974
|
const avatar = message.role === 'assistant' ? avatars?.ai : avatars?.user;
|
|
886
975
|
return (React__namespace.createElement(RoleContext.Provider, { value: message.role },
|
|
887
976
|
React__namespace.createElement(uiReact.View, { className: ui.classNames(ui.ComponentClassName.AIConversationMessage, ui.classNameModifier(ui.ComponentClassName.AIConversationMessage, variant), ui.classNameModifier(ui.ComponentClassName.AIConversationMessage, message.role)) },
|
|
888
977
|
React__namespace.createElement(uiReact.View, { className: ui.ComponentClassName.AIConversationMessageAvatar },
|
|
889
|
-
React__namespace.createElement(uiReact.Avatar,
|
|
978
|
+
React__namespace.createElement(uiReact.Avatar, { isLoading: isLoading }, avatar?.avatar)),
|
|
890
979
|
React__namespace.createElement(uiReact.View, { className: ui.ComponentClassName.AIConversationMessageBody },
|
|
891
980
|
React__namespace.createElement(MessageMeta, { message: message }),
|
|
892
981
|
React__namespace.createElement(uiReact.View, { className: ui.ComponentClassName.AIConversationMessageContent },
|
|
893
|
-
React__namespace.createElement(MessageControl, { message: message }))
|
|
982
|
+
React__namespace.createElement(MessageControl, { message: message })),
|
|
983
|
+
message.role === 'assistant' ? (React__namespace.createElement(MessageActions, { message: message })) : null))));
|
|
894
984
|
};
|
|
895
985
|
const MessageList = ({ messages, }) => {
|
|
896
986
|
const isLoading = React__namespace.useContext(LoadingContext);
|
|
@@ -898,8 +988,10 @@ const MessageList = ({ messages, }) => {
|
|
|
898
988
|
content.text ??
|
|
899
989
|
content.toolUse?.name.startsWith(RESPONSE_COMPONENT_PREFIX))) ?? [];
|
|
900
990
|
return (React__namespace.createElement(uiReact.View, { className: ui.ComponentClassName.AIConversationMessageList },
|
|
901
|
-
|
|
902
|
-
|
|
991
|
+
isLoading ? (React__namespace.createElement(React__namespace.Fragment, null,
|
|
992
|
+
React__namespace.createElement(PlaceholderMessage, { role: "user" }),
|
|
993
|
+
React__namespace.createElement(PlaceholderMessage, { role: "assistant" }))) : null,
|
|
994
|
+
messagesWithRenderableContent.map((message, i) => (React__namespace.createElement(Message, { key: `message-${i}`, message: message })))));
|
|
903
995
|
};
|
|
904
996
|
|
|
905
997
|
const Attachment = ({ file, handleRemove, }) => {
|
|
@@ -932,27 +1024,24 @@ function isHTMLFormElement(target) {
|
|
|
932
1024
|
* Will conditionally render the DropZone if allowAttachments
|
|
933
1025
|
* is true
|
|
934
1026
|
*/
|
|
935
|
-
const FormWrapper = ({ children, allowAttachments,
|
|
1027
|
+
const FormWrapper = ({ children, allowAttachments, onValidate, }) => {
|
|
936
1028
|
if (allowAttachments) {
|
|
937
1029
|
return (React__namespace.createElement(uiReact.DropZone, { className: ui.ComponentClassName.AIConversationFormDropzone, onDropComplete: ({ acceptedFiles }) => {
|
|
938
|
-
|
|
939
|
-
...prevInput,
|
|
940
|
-
files: [...(prevInput?.files ?? []), ...acceptedFiles],
|
|
941
|
-
}));
|
|
1030
|
+
onValidate(acceptedFiles);
|
|
942
1031
|
} }, children));
|
|
943
1032
|
}
|
|
944
1033
|
else {
|
|
945
1034
|
return children;
|
|
946
1035
|
}
|
|
947
1036
|
};
|
|
948
|
-
const Form = ({ setInput, input, handleSubmit, allowAttachments, }) => {
|
|
1037
|
+
const Form = ({ setInput, input, handleSubmit, allowAttachments, onValidate, isLoading, error, }) => {
|
|
949
1038
|
const icons = internal.useIcons('aiConversation');
|
|
950
1039
|
const sendIcon = icons?.send ?? React__namespace.createElement(internal.IconSend, null);
|
|
951
1040
|
const attachIcon = icons?.attach ?? React__namespace.createElement(internal.IconAttach, null);
|
|
952
1041
|
const hiddenInput = React__namespace.useRef(null);
|
|
953
|
-
const
|
|
1042
|
+
const [composing, setComposing] = React__namespace.useState(false);
|
|
954
1043
|
const isInputEmpty = !input?.text?.length && !input?.files?.length;
|
|
955
|
-
return (React__namespace.createElement(FormWrapper, {
|
|
1044
|
+
return (React__namespace.createElement(FormWrapper, { onValidate: onValidate, allowAttachments: allowAttachments },
|
|
956
1045
|
React__namespace.createElement(uiReact.View, { as: "form", className: ui.ComponentClassName.AIConversationForm, onSubmit: handleSubmit },
|
|
957
1046
|
allowAttachments ? (React__namespace.createElement(uiReact.Button, { className: ui.ComponentClassName.AIConversationFormAttach, onClick: () => {
|
|
958
1047
|
hiddenInput?.current?.click();
|
|
@@ -963,24 +1052,20 @@ const Form = ({ setInput, input, handleSubmit, allowAttachments, }) => {
|
|
|
963
1052
|
React__namespace.createElement("span", null, attachIcon),
|
|
964
1053
|
React__namespace.createElement(uiReact.VisuallyHidden, null,
|
|
965
1054
|
React__namespace.createElement("input", { type: "file", tabIndex: -1, ref: hiddenInput, onChange: (e) => {
|
|
966
|
-
|
|
967
|
-
if (!files || files.length === 0) {
|
|
1055
|
+
if (!e.target.files || e.target.files.length === 0) {
|
|
968
1056
|
return;
|
|
969
1057
|
}
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
}));
|
|
974
|
-
}, multiple: true, accept: "*", "data-testid": "hidden-file-input" })))) : null,
|
|
975
|
-
React__namespace.createElement(uiReact.TextAreaField, { className: ui.ComponentClassName.AIConversationFormField, label: "input", labelHidden: true, autoResize: true, flex: "1", rows: 1, value: input?.text ?? '', testId: "text-input", onKeyDown: (e) => {
|
|
1058
|
+
onValidate(Array.from(e.target.files));
|
|
1059
|
+
}, multiple: true, accept: ".jpeg,.png,.webp,.gif", "data-testid": "hidden-file-input" })))) : null,
|
|
1060
|
+
React__namespace.createElement(uiReact.TextAreaField, { className: ui.ComponentClassName.AIConversationFormField, label: "input", labelHidden: true, autoResize: true, flex: "1", rows: 1, value: input?.text ?? '', testId: "text-input", onCompositionStart: () => setComposing(true), onCompositionEnd: () => setComposing(false), onKeyDown: (e) => {
|
|
976
1061
|
// Submit on enter key if shift is not pressed also
|
|
977
|
-
const shouldSubmit = !e.shiftKey && e.key === 'Enter';
|
|
1062
|
+
const shouldSubmit = !e.shiftKey && e.key === 'Enter' && !composing;
|
|
978
1063
|
if (shouldSubmit && isHTMLFormElement(e.target)) {
|
|
979
1064
|
e.target.form.requestSubmit();
|
|
980
1065
|
e.preventDefault();
|
|
981
1066
|
}
|
|
982
1067
|
}, onChange: (e) => {
|
|
983
|
-
setInput((prevValue) => ({
|
|
1068
|
+
setInput?.((prevValue) => ({
|
|
984
1069
|
...prevValue,
|
|
985
1070
|
text: e.target.value,
|
|
986
1071
|
}));
|
|
@@ -990,6 +1075,7 @@ const Form = ({ setInput, input, handleSubmit, allowAttachments, }) => {
|
|
|
990
1075
|
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
|
991
1076
|
isDisabled: isLoading || isInputEmpty },
|
|
992
1077
|
React__namespace.createElement("span", null, sendIcon))),
|
|
1078
|
+
error ? (React__namespace.createElement(uiReact.Message, { className: ui.ComponentClassName.AIConversationFormError, variation: "plain", colorTheme: "warning" }, error)) : null,
|
|
993
1079
|
React__namespace.createElement(Attachments, { setInput: setInput, files: input?.files })));
|
|
994
1080
|
};
|
|
995
1081
|
|
|
@@ -1004,9 +1090,9 @@ const PromptList = ({ setInput, suggestedPrompts = [], }) => {
|
|
|
1004
1090
|
})));
|
|
1005
1091
|
};
|
|
1006
1092
|
|
|
1007
|
-
const VERSION = '
|
|
1093
|
+
const VERSION = '1.1.0';
|
|
1008
1094
|
|
|
1009
|
-
function
|
|
1095
|
+
function AIConversationBase({ avatars, controls, ...rest }) {
|
|
1010
1096
|
uiReactCore.useSetUserAgent({
|
|
1011
1097
|
componentName: 'AIConversation',
|
|
1012
1098
|
packageName: 'react-ai',
|
|
@@ -1016,83 +1102,53 @@ function Provider({ actions, avatars, controls, handleSendMessage, messages, res
|
|
|
1016
1102
|
const defaultAvatars = {
|
|
1017
1103
|
ai: {
|
|
1018
1104
|
username: 'Assistant',
|
|
1019
|
-
avatar: icons?.assistant ?? React__namespace.createElement(internal.IconAssistant,
|
|
1105
|
+
avatar: icons?.assistant ?? React__namespace.createElement(internal.IconAssistant, { testId: "icon-assistant" }),
|
|
1020
1106
|
},
|
|
1021
1107
|
user: {
|
|
1022
1108
|
username: 'User',
|
|
1023
|
-
avatar: icons?.user ?? React__namespace.createElement(internal.IconUser,
|
|
1109
|
+
avatar: icons?.user ?? React__namespace.createElement(internal.IconUser, { testId: "icon-user" }),
|
|
1024
1110
|
},
|
|
1025
1111
|
};
|
|
1026
1112
|
const providerProps = {
|
|
1027
|
-
|
|
1028
|
-
handleSendMessage,
|
|
1113
|
+
...rest,
|
|
1029
1114
|
avatars: {
|
|
1030
1115
|
...defaultAvatars,
|
|
1031
1116
|
...avatars,
|
|
1032
1117
|
},
|
|
1033
|
-
isLoading,
|
|
1034
|
-
elements: {
|
|
1035
|
-
Text: uiReact.Text,
|
|
1036
|
-
},
|
|
1037
|
-
actions,
|
|
1038
|
-
suggestedPrompts,
|
|
1039
|
-
responseComponents,
|
|
1040
|
-
variant,
|
|
1041
1118
|
controls: {
|
|
1042
1119
|
MessageList,
|
|
1043
1120
|
PromptList,
|
|
1044
1121
|
Form,
|
|
1045
1122
|
...controls,
|
|
1046
1123
|
},
|
|
1047
|
-
displayText,
|
|
1048
|
-
allowAttachments,
|
|
1049
|
-
messageRenderer,
|
|
1050
1124
|
};
|
|
1051
|
-
return (React__namespace.createElement(AIConversationProvider, { ...providerProps },
|
|
1052
|
-
}
|
|
1053
|
-
function AIConversationBase(props) {
|
|
1054
|
-
return (React__namespace.createElement(Provider, { ...props },
|
|
1055
|
-
React__namespace.createElement(uiReact.Flex, { className: ui.ComponentClassName.AIConversation },
|
|
1125
|
+
return (React__namespace.createElement(AIConversationProvider, { ...providerProps },
|
|
1126
|
+
React__namespace.createElement(uiReact.Flex, { className: ui.ComponentClassName.AIConversation, testId: "ai-conversation" },
|
|
1056
1127
|
React__namespace.createElement(uiReact.ScrollView, { autoScroll: "smooth", flex: "1" },
|
|
1057
1128
|
React__namespace.createElement(DefaultMessageControl, null),
|
|
1058
1129
|
React__namespace.createElement(MessagesControl, null)),
|
|
1059
1130
|
React__namespace.createElement(FormControl, null))));
|
|
1060
1131
|
}
|
|
1061
|
-
/**
|
|
1062
|
-
* @experimental
|
|
1063
|
-
*/
|
|
1064
1132
|
const AIConversation = Object.assign(AIConversationBase, {
|
|
1065
|
-
Provider,
|
|
1133
|
+
Provider: AIConversationProvider,
|
|
1066
1134
|
DefaultMessage: DefaultMessageControl,
|
|
1067
1135
|
Messages: MessagesControl,
|
|
1068
1136
|
Form: FormControl,
|
|
1069
1137
|
});
|
|
1070
1138
|
|
|
1071
|
-
const AIContext = React__namespace["default"].createContext(undefined);
|
|
1072
|
-
const useAIContext = () => {
|
|
1073
|
-
const context = React__namespace["default"].useContext(AIContext);
|
|
1074
|
-
const [routeToConversationsMap, setRouteToConversationsMap] = React__namespace["default"].useState({});
|
|
1075
|
-
if (context) {
|
|
1076
|
-
return context;
|
|
1077
|
-
}
|
|
1078
|
-
return { routeToConversationsMap, setRouteToConversationsMap };
|
|
1079
|
-
};
|
|
1080
|
-
/**
|
|
1081
|
-
* @experimental
|
|
1082
|
-
*/
|
|
1083
|
-
const AIContextProvider = ({ children, }) => {
|
|
1084
|
-
const context = useAIContext();
|
|
1085
|
-
return React__namespace["default"].createElement(AIContext.Provider, { value: context }, children);
|
|
1086
|
-
};
|
|
1087
|
-
|
|
1088
1139
|
// default state
|
|
1089
1140
|
const INITIAL_STATE = {
|
|
1090
1141
|
hasError: false,
|
|
1091
1142
|
isLoading: false,
|
|
1092
1143
|
messages: undefined,
|
|
1093
1144
|
};
|
|
1094
|
-
const LOADING_STATE = {
|
|
1145
|
+
const LOADING_STATE = {
|
|
1146
|
+
hasError: false,
|
|
1147
|
+
isLoading: true,
|
|
1148
|
+
messages: undefined,
|
|
1149
|
+
};
|
|
1095
1150
|
const ERROR_STATE = { hasError: true, isLoading: false };
|
|
1151
|
+
|
|
1096
1152
|
function createUseAIGeneration(client) {
|
|
1097
1153
|
const useAIGeneration = (routeName) => {
|
|
1098
1154
|
const [dataState, setDataState] = React__namespace.useState(() => ({
|
|
@@ -1119,142 +1175,314 @@ function createUseAIGeneration(client) {
|
|
|
1119
1175
|
return useAIGeneration;
|
|
1120
1176
|
}
|
|
1121
1177
|
|
|
1122
|
-
|
|
1178
|
+
const contentFromEvents = (contentBlocks) => {
|
|
1179
|
+
if (!contentBlocks)
|
|
1180
|
+
return [];
|
|
1181
|
+
return contentBlocks.map((contentBlock) => {
|
|
1182
|
+
const isTextBlock = contentBlock.some((event) => event.text);
|
|
1183
|
+
if (isTextBlock) {
|
|
1184
|
+
return {
|
|
1185
|
+
text: contentBlock
|
|
1186
|
+
.map((event) => {
|
|
1187
|
+
return event.text;
|
|
1188
|
+
})
|
|
1189
|
+
.join(''),
|
|
1190
|
+
};
|
|
1191
|
+
}
|
|
1192
|
+
// tool use is never chunked
|
|
1193
|
+
if (contentBlock[0].toolUse) {
|
|
1194
|
+
return { toolUse: contentBlock[0].toolUse };
|
|
1195
|
+
}
|
|
1196
|
+
});
|
|
1197
|
+
};
|
|
1198
|
+
|
|
1199
|
+
async function exhaustivelyListMessages({ conversation, messages = [], nextToken, }) {
|
|
1200
|
+
const result = await conversation.listMessages({ nextToken });
|
|
1201
|
+
if (result.data) {
|
|
1202
|
+
messages?.push(...result.data);
|
|
1203
|
+
}
|
|
1204
|
+
if (result.nextToken) {
|
|
1205
|
+
return exhaustivelyListMessages({
|
|
1206
|
+
conversation,
|
|
1207
|
+
messages,
|
|
1208
|
+
nextToken: result.nextToken,
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1123
1211
|
return {
|
|
1124
|
-
...
|
|
1125
|
-
|
|
1126
|
-
...previousValue[routeName],
|
|
1127
|
-
[conversationId]: messages,
|
|
1128
|
-
},
|
|
1212
|
+
...result,
|
|
1213
|
+
data: messages,
|
|
1129
1214
|
};
|
|
1130
1215
|
}
|
|
1216
|
+
|
|
1217
|
+
function hasStarted(state) {
|
|
1218
|
+
return ['initialLoading', 'initialized'].includes(state);
|
|
1219
|
+
}
|
|
1131
1220
|
function createUseAIConversation(client) {
|
|
1221
|
+
// This is a bit complicated so buckle up.
|
|
1222
|
+
// The way the data client works is conversation.get() or conversation.create()
|
|
1223
|
+
// is an async function because it makes a graphql call to appsync
|
|
1224
|
+
// then it returns a conversation object, which is like a normal
|
|
1225
|
+
// data client record, except that it also has functions on it,
|
|
1226
|
+
// like sendMessage and onStreamEvent. onStreamEvent sets up a
|
|
1227
|
+
// subscription using a websocket connection, which ideally we only want to
|
|
1228
|
+
// do once per conversation. Because we can only subscribe AFTER the
|
|
1229
|
+
// async call to get/create the conversation is made, the cleanup
|
|
1230
|
+
// function in the effect will won't actually unsubscribe
|
|
1132
1231
|
const useAIConversation = (routeName, input = {}) => {
|
|
1133
1232
|
const clientRoute = client.conversations[routeName];
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
const
|
|
1141
|
-
const [
|
|
1142
|
-
|
|
1143
|
-
|
|
1233
|
+
// We need to keep track of the stream events as the come in
|
|
1234
|
+
// for an assistant message, but don't need to keep them in state
|
|
1235
|
+
const contentBlocksRef = React__namespace["default"].useRef();
|
|
1236
|
+
// Using this hook without an existing conversation id means
|
|
1237
|
+
// it will create a new conversation when it is executed
|
|
1238
|
+
// we don't want to create 2 conversations
|
|
1239
|
+
const initRef = React__namespace["default"].useRef('initial');
|
|
1240
|
+
const [dataState, setDataState] = React__namespace["default"].useState(() => ({
|
|
1241
|
+
...INITIAL_STATE,
|
|
1242
|
+
data: { messages: [], conversation: undefined },
|
|
1243
|
+
}));
|
|
1244
|
+
const { conversation } = dataState.data;
|
|
1245
|
+
const { id, onInitialize, onMessage } = input;
|
|
1144
1246
|
React__namespace["default"].useEffect(() => {
|
|
1145
1247
|
async function initialize() {
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1248
|
+
// We don't want to run the effect multiple times
|
|
1249
|
+
// because that could create multiple conversation records
|
|
1250
|
+
if (hasStarted(initRef.current))
|
|
1251
|
+
return;
|
|
1252
|
+
initRef.current = 'initialLoading';
|
|
1253
|
+
// Only show component loading state if we are
|
|
1254
|
+
// actually loading messages
|
|
1255
|
+
if (id) {
|
|
1256
|
+
setDataState({
|
|
1257
|
+
...LOADING_STATE,
|
|
1258
|
+
data: { messages: [], conversation: undefined },
|
|
1259
|
+
});
|
|
1154
1260
|
}
|
|
1155
|
-
const { data:
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
messages,
|
|
1261
|
+
const { data: conversation, errors } = id
|
|
1262
|
+
? await clientRoute.get({ id })
|
|
1263
|
+
: await clientRoute.create();
|
|
1264
|
+
if (errors ?? !conversation) {
|
|
1265
|
+
setDataState({
|
|
1266
|
+
...ERROR_STATE,
|
|
1267
|
+
data: { messages: [] },
|
|
1268
|
+
messages: errors,
|
|
1164
1269
|
});
|
|
1270
|
+
}
|
|
1271
|
+
else {
|
|
1272
|
+
if (id) {
|
|
1273
|
+
const { data: messages } = await exhaustivelyListMessages({
|
|
1274
|
+
conversation,
|
|
1275
|
+
});
|
|
1276
|
+
setDataState({
|
|
1277
|
+
...INITIAL_STATE,
|
|
1278
|
+
data: { messages, conversation },
|
|
1279
|
+
});
|
|
1280
|
+
}
|
|
1281
|
+
else {
|
|
1282
|
+
setDataState({
|
|
1283
|
+
...INITIAL_STATE,
|
|
1284
|
+
data: { conversation, messages: [] },
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
initRef.current = 'initialized';
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
// this is a runtime guard to make catch an error if
|
|
1291
|
+
// the route name wrong, or there is a mismatch
|
|
1292
|
+
// between the gen2 schema definition and
|
|
1293
|
+
// whats in amplify_outputs
|
|
1294
|
+
if (!clientRoute) {
|
|
1295
|
+
setDataState({
|
|
1296
|
+
...ERROR_STATE,
|
|
1297
|
+
data: { messages: [] },
|
|
1298
|
+
messages: [
|
|
1299
|
+
{
|
|
1300
|
+
message: 'Conversation route does not exist',
|
|
1301
|
+
errorInfo: null,
|
|
1302
|
+
errorType: '',
|
|
1303
|
+
},
|
|
1304
|
+
],
|
|
1165
1305
|
});
|
|
1306
|
+
return;
|
|
1166
1307
|
}
|
|
1167
1308
|
initialize();
|
|
1168
|
-
|
|
1169
|
-
|
|
1309
|
+
return () => {
|
|
1310
|
+
contentBlocksRef.current = undefined;
|
|
1311
|
+
if (hasStarted(initRef.current))
|
|
1312
|
+
return;
|
|
1313
|
+
setDataState({
|
|
1314
|
+
...INITIAL_STATE,
|
|
1315
|
+
data: { messages: [], conversation: undefined },
|
|
1316
|
+
});
|
|
1317
|
+
};
|
|
1318
|
+
}, [clientRoute, id, setDataState]);
|
|
1319
|
+
// Run a separate effect that is triggered by the conversation state
|
|
1320
|
+
// so that we know we have a conversation object to set up the subscription
|
|
1321
|
+
// and also unsubscribe on cleanup
|
|
1170
1322
|
React__namespace["default"].useEffect(() => {
|
|
1171
|
-
if (
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1323
|
+
if (!conversation)
|
|
1324
|
+
return;
|
|
1325
|
+
const subscription = conversation.onStreamEvent({
|
|
1326
|
+
next: (event) => {
|
|
1327
|
+
const {
|
|
1328
|
+
// messages have a content block array,
|
|
1329
|
+
// this is the index of the content block that was updated
|
|
1330
|
+
contentBlockIndex,
|
|
1331
|
+
// this is the index of the content chunk, ensure these are in order!
|
|
1332
|
+
contentBlockDeltaIndex,
|
|
1333
|
+
// this is sent after the last content chunk, verify this matches the
|
|
1334
|
+
// previous contentBlockDeltaIndex
|
|
1335
|
+
contentBlockDoneAtIndex,
|
|
1336
|
+
// this is the final event of the conversation turn
|
|
1337
|
+
stopReason, conversationId, id, } = event;
|
|
1338
|
+
// return early for content blocks being done
|
|
1339
|
+
// or conversation turn being over
|
|
1340
|
+
if (contentBlockDoneAtIndex) {
|
|
1341
|
+
return;
|
|
1342
|
+
}
|
|
1343
|
+
// stop reason will signify end of conversation turn
|
|
1344
|
+
if (stopReason) {
|
|
1345
|
+
// remove loading state from streamed message
|
|
1346
|
+
setDataState((prev) => {
|
|
1347
|
+
return {
|
|
1348
|
+
...prev,
|
|
1349
|
+
data: {
|
|
1350
|
+
...prev.data,
|
|
1351
|
+
messages: prev.data.messages.map((message) => ({
|
|
1352
|
+
...message,
|
|
1353
|
+
isLoading: false,
|
|
1354
|
+
})),
|
|
1355
|
+
},
|
|
1356
|
+
};
|
|
1195
1357
|
});
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
}, [conversation, routeName, setRouteToConversationsMap]);
|
|
1204
|
-
const subscribe = React__namespace["default"].useCallback((handleStoreChange) => {
|
|
1205
|
-
const subscription = conversation &&
|
|
1206
|
-
conversation.onMessage((message) => {
|
|
1207
|
-
if (input.onResponse)
|
|
1208
|
-
input.onResponse(message);
|
|
1209
|
-
setWaitingForAIResponse(false);
|
|
1210
|
-
setLocalMessages((previousLocalMessages) => [
|
|
1211
|
-
...previousLocalMessages,
|
|
1212
|
-
message,
|
|
1213
|
-
]);
|
|
1214
|
-
setRouteToConversationsMap((previousValue) => {
|
|
1215
|
-
return createNewConversationMessageInRoute({
|
|
1216
|
-
previousValue,
|
|
1217
|
-
routeName: routeName,
|
|
1218
|
-
conversationId: conversation.id,
|
|
1219
|
-
messages: [
|
|
1220
|
-
...previousValue[routeName][conversation.id],
|
|
1221
|
-
message,
|
|
1222
|
-
],
|
|
1358
|
+
onMessage?.({
|
|
1359
|
+
id,
|
|
1360
|
+
conversationId,
|
|
1361
|
+
content: contentFromEvents(contentBlocksRef.current),
|
|
1362
|
+
createdAt: new Date().toISOString(),
|
|
1363
|
+
role: 'assistant',
|
|
1364
|
+
isLoading: true,
|
|
1223
1365
|
});
|
|
1366
|
+
// clear out the stream cache
|
|
1367
|
+
contentBlocksRef.current = undefined;
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
// no ref means its the first event for the message stream
|
|
1371
|
+
// so lets create the contentBlocks ref or else we will
|
|
1372
|
+
// add the incoming event to the right content content block
|
|
1373
|
+
if (!contentBlocksRef.current) {
|
|
1374
|
+
contentBlocksRef.current = [[event]];
|
|
1375
|
+
}
|
|
1376
|
+
else {
|
|
1377
|
+
// place the incoming event in the right content block
|
|
1378
|
+
// and order. message content is an array so a single message
|
|
1379
|
+
// can have multiple content blocks, and each content block
|
|
1380
|
+
// can have multiple events/chunks
|
|
1381
|
+
const currentBlock = contentBlocksRef.current[contentBlockIndex];
|
|
1382
|
+
if (!currentBlock) {
|
|
1383
|
+
contentBlocksRef.current[contentBlockIndex] = [event];
|
|
1384
|
+
}
|
|
1385
|
+
else {
|
|
1386
|
+
contentBlocksRef.current[contentBlockIndex] = [
|
|
1387
|
+
...currentBlock.slice(0, contentBlockDeltaIndex),
|
|
1388
|
+
event,
|
|
1389
|
+
...currentBlock.slice(contentBlockDeltaIndex),
|
|
1390
|
+
];
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
setDataState((prev) => {
|
|
1394
|
+
const message = {
|
|
1395
|
+
id,
|
|
1396
|
+
conversationId,
|
|
1397
|
+
content: contentFromEvents(contentBlocksRef.current),
|
|
1398
|
+
createdAt: new Date().toISOString(),
|
|
1399
|
+
role: 'assistant',
|
|
1400
|
+
isLoading: true,
|
|
1401
|
+
};
|
|
1402
|
+
return {
|
|
1403
|
+
...prev,
|
|
1404
|
+
data: {
|
|
1405
|
+
...prev.data,
|
|
1406
|
+
// TODO: we are assuming we only update the last
|
|
1407
|
+
// message, but maybe we should match it by message ID?
|
|
1408
|
+
messages: [...prev.data.messages.slice(0, -1), message],
|
|
1409
|
+
},
|
|
1410
|
+
};
|
|
1224
1411
|
});
|
|
1225
|
-
|
|
1226
|
-
|
|
1412
|
+
},
|
|
1413
|
+
error: (error) => {
|
|
1414
|
+
setDataState((prev) => {
|
|
1415
|
+
return {
|
|
1416
|
+
...prev,
|
|
1417
|
+
...ERROR_STATE,
|
|
1418
|
+
messages: error.errors,
|
|
1419
|
+
};
|
|
1420
|
+
});
|
|
1421
|
+
},
|
|
1422
|
+
});
|
|
1423
|
+
if (ui.isFunction(onInitialize)) {
|
|
1424
|
+
onInitialize(conversation);
|
|
1425
|
+
}
|
|
1227
1426
|
return () => {
|
|
1228
|
-
|
|
1427
|
+
contentBlocksRef.current = undefined;
|
|
1428
|
+
subscription.unsubscribe();
|
|
1229
1429
|
};
|
|
1230
|
-
}, [conversation,
|
|
1231
|
-
const
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1430
|
+
}, [conversation, onInitialize, onMessage, setDataState]);
|
|
1431
|
+
const handleSendMessage = React__namespace["default"].useCallback((input) => {
|
|
1432
|
+
const { content } = input;
|
|
1433
|
+
if (conversation) {
|
|
1434
|
+
setDataState((prevState) => ({
|
|
1435
|
+
...prevState,
|
|
1436
|
+
data: {
|
|
1437
|
+
...prevState.data,
|
|
1438
|
+
// optimistically add user and assistant messages
|
|
1439
|
+
messages: [
|
|
1440
|
+
...prevState.data.messages,
|
|
1441
|
+
{
|
|
1442
|
+
content,
|
|
1443
|
+
role: 'user',
|
|
1444
|
+
createdAt: new Date().toISOString(),
|
|
1445
|
+
id: 'temp-id',
|
|
1446
|
+
conversationId: conversation.id ?? '',
|
|
1447
|
+
},
|
|
1448
|
+
{
|
|
1449
|
+
content: [{ text: ' ' }],
|
|
1450
|
+
role: 'assistant',
|
|
1451
|
+
createdAt: new Date().toISOString(),
|
|
1452
|
+
id: 'temp-id-2',
|
|
1453
|
+
conversationId: conversation.id ?? '',
|
|
1454
|
+
isLoading: true,
|
|
1455
|
+
},
|
|
1456
|
+
],
|
|
1457
|
+
},
|
|
1458
|
+
}));
|
|
1459
|
+
conversation.sendMessage(input);
|
|
1460
|
+
}
|
|
1461
|
+
else {
|
|
1462
|
+
setDataState((prev) => ({
|
|
1463
|
+
...prev,
|
|
1464
|
+
...ERROR_STATE,
|
|
1465
|
+
messages: [
|
|
1466
|
+
{
|
|
1467
|
+
message: 'No conversation found',
|
|
1468
|
+
errorInfo: null,
|
|
1469
|
+
errorType: '',
|
|
1470
|
+
},
|
|
1471
|
+
],
|
|
1472
|
+
}));
|
|
1473
|
+
}
|
|
1474
|
+
}, [conversation]);
|
|
1475
|
+
return [dataState, handleSendMessage];
|
|
1244
1476
|
};
|
|
1245
1477
|
return useAIConversation;
|
|
1246
1478
|
}
|
|
1247
1479
|
|
|
1248
|
-
/**
|
|
1249
|
-
* @experimental
|
|
1250
|
-
*/
|
|
1251
1480
|
function createAIHooks(_client) {
|
|
1252
1481
|
const useAIConversation = createUseAIConversation(_client);
|
|
1253
1482
|
const useAIGeneration = createUseAIGeneration(_client);
|
|
1254
1483
|
return { useAIConversation, useAIGeneration };
|
|
1255
1484
|
}
|
|
1256
1485
|
|
|
1257
|
-
exports.AIContextProvider = AIContextProvider;
|
|
1258
1486
|
exports.AIConversation = AIConversation;
|
|
1259
1487
|
exports.createAIConversation = createAIConversation;
|
|
1260
1488
|
exports.createAIHooks = createAIHooks;
|