@aws-amplify/ui-react-ai 0.3.2 → 1.0.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 +21 -33
- package/dist/esm/components/AIConversation/AIConversationProvider.mjs +22 -16
- package/dist/esm/components/AIConversation/context/AIContextContext.mjs +8 -0
- package/dist/esm/components/AIConversation/context/FallbackComponentContext.mjs +8 -0
- package/dist/esm/components/AIConversation/context/MessageRenderContext.mjs +9 -0
- package/dist/esm/components/AIConversation/context/ResponseComponentsContext.mjs +6 -2
- package/dist/esm/components/AIConversation/context/WelcomeMessageContext.mjs +8 -0
- package/dist/esm/components/AIConversation/createAIConversation.mjs +18 -22
- package/dist/esm/components/AIConversation/views/Controls/ActionsBarControl.mjs +6 -2
- package/dist/esm/components/AIConversation/views/Controls/AttachFileControl.mjs +5 -0
- package/dist/esm/components/AIConversation/views/Controls/AttachmentListControl.mjs +5 -0
- package/dist/esm/components/AIConversation/views/Controls/AvatarControl.mjs +5 -0
- package/dist/esm/components/AIConversation/views/Controls/DefaultMessageControl.mjs +31 -0
- package/dist/esm/components/AIConversation/views/Controls/{FieldControl.mjs → FormControl.mjs} +22 -13
- package/dist/esm/components/AIConversation/views/Controls/MessagesControl.mjs +42 -42
- package/dist/esm/components/AIConversation/views/Controls/PromptControl.mjs +9 -36
- package/dist/esm/components/AIConversation/views/default/Form.mjs +4 -5
- package/dist/esm/components/AIConversation/views/default/MessageList.mjs +34 -16
- package/dist/esm/components/AIConversation/views/default/PromptList.mjs +1 -1
- 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 +3 -0
- package/dist/index.js +569 -444
- package/dist/types/components/AIConversation/AIConversation.d.ts +2 -19
- package/dist/types/components/AIConversation/AIConversationProvider.d.ts +2 -3
- package/dist/types/components/AIConversation/context/AIContextContext.d.ts +6 -0
- package/dist/types/components/AIConversation/context/ControlsContext.d.ts +1 -0
- package/dist/types/components/AIConversation/context/FallbackComponentContext.d.ts +7 -0
- package/dist/types/components/AIConversation/context/MessageRenderContext.d.ts +5 -0
- package/dist/types/components/AIConversation/context/ResponseComponentsContext.d.ts +2 -2
- package/dist/types/components/AIConversation/context/WelcomeMessageContext.d.ts +8 -0
- package/dist/types/components/AIConversation/context/elements/definitions.d.ts +1 -1
- package/dist/types/components/AIConversation/context/index.d.ts +5 -0
- package/dist/types/components/AIConversation/createAIConversation.d.ts +0 -3
- package/dist/types/components/AIConversation/index.d.ts +2 -1
- package/dist/types/components/AIConversation/types.d.ts +24 -36
- package/dist/types/components/AIConversation/utils.d.ts +2 -2
- package/dist/types/components/AIConversation/views/Controls/DefaultMessageControl.d.ts +2 -0
- package/dist/types/components/AIConversation/views/Controls/{FieldControl.d.ts → FormControl.d.ts} +2 -2
- package/dist/types/components/AIConversation/views/Controls/MessagesControl.d.ts +3 -9
- package/dist/types/components/AIConversation/views/Controls/PromptControl.d.ts +0 -3
- package/dist/types/components/AIConversation/views/Controls/index.d.ts +3 -4
- 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/components/AIConversation/views/index.d.ts +2 -3
- 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 +38 -7
- package/dist/types/version.d.ts +1 -0
- package/package.json +20 -6
- package/dist/ai-conversation-styles.css +0 -195
- package/dist/ai-conversation-styles.js +0 -2
- package/dist/esm/components/AIConversation/views/Controls/HeaderControl.mjs +0 -34
- package/dist/esm/components/AIConversation/views/ConversationView.mjs +0 -20
- package/dist/esm/hooks/AIContextProvider.mjs +0 -20
- package/dist/types/ai-conversation-styles.d.ts +0 -1
- package/dist/types/components/AIConversation/views/Controls/HeaderControl.d.ts +0 -9
- package/dist/types/components/AIConversation/views/ConversationView.d.ts +0 -2
- package/dist/types/hooks/AIContextProvider.d.ts +0 -17
|
@@ -1,128 +1,268 @@
|
|
|
1
1
|
import React__default from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { INITIAL_STATE, ERROR_STATE, LOADING_STATE } from './shared.mjs';
|
|
3
|
+
import { isFunction } from '@aws-amplify/ui';
|
|
4
|
+
import { contentFromEvents } from './contentFromEvents.mjs';
|
|
5
|
+
import { exhaustivelyListMessages } from './exhaustivelyListMessages.mjs';
|
|
3
6
|
|
|
4
|
-
function
|
|
5
|
-
return
|
|
6
|
-
...previousValue,
|
|
7
|
-
[routeName]: {
|
|
8
|
-
...previousValue[routeName],
|
|
9
|
-
[conversationId]: messages,
|
|
10
|
-
},
|
|
11
|
-
};
|
|
7
|
+
function hasStarted(state) {
|
|
8
|
+
return ['initialLoading', 'initialized'].includes(state);
|
|
12
9
|
}
|
|
13
10
|
function createUseAIConversation(client) {
|
|
11
|
+
// This is a bit complicated so buckle up.
|
|
12
|
+
// The way the data client works is conversation.get() or conversation.create()
|
|
13
|
+
// is an async function because it makes a graphql call to appsync
|
|
14
|
+
// then it returns a conversation object, which is like a normal
|
|
15
|
+
// data client record, except that it also has functions on it,
|
|
16
|
+
// like sendMessage and onStreamEvent. onStreamEvent sets up a
|
|
17
|
+
// subscription using a websocket connection, which ideally we only want to
|
|
18
|
+
// do once per conversation. Because we can only subscribe AFTER the
|
|
19
|
+
// async call to get/create the conversation is made, the cleanup
|
|
20
|
+
// function in the effect will won't actually unsubscribe
|
|
14
21
|
const useAIConversation = (routeName, input = {}) => {
|
|
15
22
|
const clientRoute = client.conversations[routeName];
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
const [
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
// We need to keep track of the stream events as the come in
|
|
24
|
+
// for an assistant message, but don't need to keep them in state
|
|
25
|
+
const contentBlocksRef = React__default.useRef();
|
|
26
|
+
// Using this hook without an existing conversation id means
|
|
27
|
+
// it will create a new conversation when it is executed
|
|
28
|
+
// we don't want to create 2 conversations
|
|
29
|
+
const initRef = React__default.useRef('initial');
|
|
30
|
+
const [dataState, setDataState] = React__default.useState(() => ({
|
|
31
|
+
...INITIAL_STATE,
|
|
32
|
+
data: { messages: [], conversation: undefined },
|
|
33
|
+
}));
|
|
34
|
+
const { conversation } = dataState.data;
|
|
35
|
+
const { id, onInitialize, onMessage } = input;
|
|
26
36
|
React__default.useEffect(() => {
|
|
27
37
|
async function initialize() {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
// We don't want to run the effect multiple times
|
|
39
|
+
// because that could create multiple conversation records
|
|
40
|
+
if (hasStarted(initRef.current))
|
|
41
|
+
return;
|
|
42
|
+
initRef.current = 'initialLoading';
|
|
43
|
+
// Only show component loading state if we are
|
|
44
|
+
// actually loading messages
|
|
45
|
+
if (id) {
|
|
46
|
+
setDataState({
|
|
47
|
+
...LOADING_STATE,
|
|
48
|
+
data: { messages: [], conversation: undefined },
|
|
49
|
+
});
|
|
36
50
|
}
|
|
37
|
-
const { data:
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
messages,
|
|
51
|
+
const { data: conversation, errors } = id
|
|
52
|
+
? await clientRoute.get({ id })
|
|
53
|
+
: await clientRoute.create();
|
|
54
|
+
if (errors ?? !conversation) {
|
|
55
|
+
setDataState({
|
|
56
|
+
...ERROR_STATE,
|
|
57
|
+
data: { messages: [] },
|
|
58
|
+
messages: errors,
|
|
46
59
|
});
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
if (id) {
|
|
63
|
+
const { data: messages } = await exhaustivelyListMessages({
|
|
64
|
+
conversation,
|
|
65
|
+
});
|
|
66
|
+
setDataState({
|
|
67
|
+
...INITIAL_STATE,
|
|
68
|
+
data: { messages, conversation },
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
setDataState({
|
|
73
|
+
...INITIAL_STATE,
|
|
74
|
+
data: { conversation, messages: [] },
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
initRef.current = 'initialized';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// this is a runtime guard to make catch an error if
|
|
81
|
+
// the route name wrong, or there is a mismatch
|
|
82
|
+
// between the gen2 schema definition and
|
|
83
|
+
// whats in amplify_outputs
|
|
84
|
+
if (!clientRoute) {
|
|
85
|
+
setDataState({
|
|
86
|
+
...ERROR_STATE,
|
|
87
|
+
data: { messages: [] },
|
|
88
|
+
messages: [
|
|
89
|
+
{
|
|
90
|
+
message: 'Conversation route does not exist',
|
|
91
|
+
errorInfo: null,
|
|
92
|
+
errorType: '',
|
|
93
|
+
},
|
|
94
|
+
],
|
|
47
95
|
});
|
|
96
|
+
return;
|
|
48
97
|
}
|
|
49
98
|
initialize();
|
|
50
|
-
|
|
51
|
-
|
|
99
|
+
return () => {
|
|
100
|
+
contentBlocksRef.current = undefined;
|
|
101
|
+
if (hasStarted(initRef.current))
|
|
102
|
+
return;
|
|
103
|
+
setDataState({
|
|
104
|
+
...INITIAL_STATE,
|
|
105
|
+
data: { messages: [], conversation: undefined },
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
}, [clientRoute, id, setDataState]);
|
|
109
|
+
// Run a separate effect that is triggered by the conversation state
|
|
110
|
+
// so that we know we have a conversation object to set up the subscription
|
|
111
|
+
// and also unsubscribe on cleanup
|
|
52
112
|
React__default.useEffect(() => {
|
|
53
|
-
if (
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
113
|
+
if (!conversation)
|
|
114
|
+
return;
|
|
115
|
+
const subscription = conversation.onStreamEvent({
|
|
116
|
+
next: (event) => {
|
|
117
|
+
const {
|
|
118
|
+
// messages have a content block array,
|
|
119
|
+
// this is the index of the content block that was updated
|
|
120
|
+
contentBlockIndex,
|
|
121
|
+
// this is the index of the content chunk, ensure these are in order!
|
|
122
|
+
contentBlockDeltaIndex,
|
|
123
|
+
// this is sent after the last content chunk, verify this matches the
|
|
124
|
+
// previous contentBlockDeltaIndex
|
|
125
|
+
contentBlockDoneAtIndex,
|
|
126
|
+
// this is the final event of the conversation turn
|
|
127
|
+
stopReason, conversationId, id, } = event;
|
|
128
|
+
// return early for content blocks being done
|
|
129
|
+
// or conversation turn being over
|
|
130
|
+
if (contentBlockDoneAtIndex) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
// stop reason will signify end of conversation turn
|
|
134
|
+
if (stopReason) {
|
|
135
|
+
// remove loading state from streamed message
|
|
136
|
+
setDataState((prev) => {
|
|
137
|
+
return {
|
|
138
|
+
...prev,
|
|
139
|
+
data: {
|
|
140
|
+
...prev.data,
|
|
141
|
+
messages: prev.data.messages.map((message) => ({
|
|
142
|
+
...message,
|
|
143
|
+
isLoading: false,
|
|
144
|
+
})),
|
|
145
|
+
},
|
|
146
|
+
};
|
|
77
147
|
});
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}, [conversation, routeName, setRouteToConversationsMap]);
|
|
86
|
-
const subscribe = React__default.useCallback((handleStoreChange) => {
|
|
87
|
-
const subscription = conversation &&
|
|
88
|
-
conversation.onMessage((message) => {
|
|
89
|
-
if (input.onResponse)
|
|
90
|
-
input.onResponse(message);
|
|
91
|
-
setWaitingForAIResponse(false);
|
|
92
|
-
setLocalMessages((previousLocalMessages) => [
|
|
93
|
-
...previousLocalMessages,
|
|
94
|
-
message,
|
|
95
|
-
]);
|
|
96
|
-
setRouteToConversationsMap((previousValue) => {
|
|
97
|
-
return createNewConversationMessageInRoute({
|
|
98
|
-
previousValue,
|
|
99
|
-
routeName: routeName,
|
|
100
|
-
conversationId: conversation.id,
|
|
101
|
-
messages: [
|
|
102
|
-
...previousValue[routeName][conversation.id],
|
|
103
|
-
message,
|
|
104
|
-
],
|
|
148
|
+
onMessage?.({
|
|
149
|
+
id,
|
|
150
|
+
conversationId,
|
|
151
|
+
content: contentFromEvents(contentBlocksRef.current),
|
|
152
|
+
createdAt: new Date().toISOString(),
|
|
153
|
+
role: 'assistant',
|
|
154
|
+
isLoading: true,
|
|
105
155
|
});
|
|
156
|
+
// clear out the stream cache
|
|
157
|
+
contentBlocksRef.current = undefined;
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
// no ref means its the first event for the message stream
|
|
161
|
+
// so lets create the contentBlocks ref or else we will
|
|
162
|
+
// add the incoming event to the right content content block
|
|
163
|
+
if (!contentBlocksRef.current) {
|
|
164
|
+
contentBlocksRef.current = [[event]];
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
// place the incoming event in the right content block
|
|
168
|
+
// and order. message content is an array so a single message
|
|
169
|
+
// can have multiple content blocks, and each content block
|
|
170
|
+
// can have multiple events/chunks
|
|
171
|
+
const currentBlock = contentBlocksRef.current[contentBlockIndex];
|
|
172
|
+
if (!currentBlock) {
|
|
173
|
+
contentBlocksRef.current[contentBlockIndex] = [event];
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
contentBlocksRef.current[contentBlockIndex] = [
|
|
177
|
+
...currentBlock.slice(0, contentBlockDeltaIndex),
|
|
178
|
+
event,
|
|
179
|
+
...currentBlock.slice(contentBlockDeltaIndex),
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
setDataState((prev) => {
|
|
184
|
+
const message = {
|
|
185
|
+
id,
|
|
186
|
+
conversationId,
|
|
187
|
+
content: contentFromEvents(contentBlocksRef.current),
|
|
188
|
+
createdAt: new Date().toISOString(),
|
|
189
|
+
role: 'assistant',
|
|
190
|
+
isLoading: true,
|
|
191
|
+
};
|
|
192
|
+
return {
|
|
193
|
+
...prev,
|
|
194
|
+
data: {
|
|
195
|
+
...prev.data,
|
|
196
|
+
// TODO: we are assuming we only update the last
|
|
197
|
+
// message, but maybe we should match it by message ID?
|
|
198
|
+
messages: [...prev.data.messages.slice(0, -1), message],
|
|
199
|
+
},
|
|
200
|
+
};
|
|
106
201
|
});
|
|
107
|
-
|
|
108
|
-
|
|
202
|
+
},
|
|
203
|
+
error: (error) => {
|
|
204
|
+
setDataState((prev) => {
|
|
205
|
+
return {
|
|
206
|
+
...prev,
|
|
207
|
+
...ERROR_STATE,
|
|
208
|
+
messages: error.errors,
|
|
209
|
+
};
|
|
210
|
+
});
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
if (isFunction(onInitialize)) {
|
|
214
|
+
onInitialize(conversation);
|
|
215
|
+
}
|
|
109
216
|
return () => {
|
|
110
|
-
|
|
217
|
+
contentBlocksRef.current = undefined;
|
|
218
|
+
subscription.unsubscribe();
|
|
111
219
|
};
|
|
112
|
-
}, [conversation,
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
220
|
+
}, [conversation, onInitialize, onMessage, setDataState]);
|
|
221
|
+
const handleSendMessage = React__default.useCallback((input) => {
|
|
222
|
+
const { content } = input;
|
|
223
|
+
if (conversation) {
|
|
224
|
+
setDataState((prevState) => ({
|
|
225
|
+
...prevState,
|
|
226
|
+
data: {
|
|
227
|
+
...prevState.data,
|
|
228
|
+
// optimistically add user and assistant messages
|
|
229
|
+
messages: [
|
|
230
|
+
...prevState.data.messages,
|
|
231
|
+
{
|
|
232
|
+
content,
|
|
233
|
+
role: 'user',
|
|
234
|
+
createdAt: new Date().toISOString(),
|
|
235
|
+
id: 'temp-id',
|
|
236
|
+
conversationId: conversation.id ?? '',
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
content: [{ text: ' ' }],
|
|
240
|
+
role: 'assistant',
|
|
241
|
+
createdAt: new Date().toISOString(),
|
|
242
|
+
id: 'temp-id-2',
|
|
243
|
+
conversationId: conversation.id ?? '',
|
|
244
|
+
isLoading: true,
|
|
245
|
+
},
|
|
246
|
+
],
|
|
247
|
+
},
|
|
248
|
+
}));
|
|
249
|
+
conversation.sendMessage(input);
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
setDataState((prev) => ({
|
|
253
|
+
...prev,
|
|
254
|
+
...ERROR_STATE,
|
|
255
|
+
messages: [
|
|
256
|
+
{
|
|
257
|
+
message: 'No conversation found',
|
|
258
|
+
errorInfo: null,
|
|
259
|
+
errorType: '',
|
|
260
|
+
},
|
|
261
|
+
],
|
|
262
|
+
}));
|
|
263
|
+
}
|
|
264
|
+
}, [conversation]);
|
|
265
|
+
return [dataState, handleSendMessage];
|
|
126
266
|
};
|
|
127
267
|
return useAIConversation;
|
|
128
268
|
}
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import * as React from 'react';
|
|
2
|
+
import { INITIAL_STATE, LOADING_STATE, ERROR_STATE } from './shared.mjs';
|
|
2
3
|
|
|
3
|
-
// default state
|
|
4
|
-
const INITIAL_STATE = {
|
|
5
|
-
hasError: false,
|
|
6
|
-
isLoading: false,
|
|
7
|
-
messages: undefined,
|
|
8
|
-
};
|
|
9
|
-
const LOADING_STATE = { hasError: false, isLoading: true, messages: undefined };
|
|
10
|
-
const ERROR_STATE = { hasError: true, isLoading: false };
|
|
11
4
|
function createUseAIGeneration(client) {
|
|
12
5
|
const useAIGeneration = (routeName) => {
|
|
13
6
|
const [dataState, setDataState] = React.useState(() => ({
|
package/dist/esm/index.mjs
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
export { createAIConversation } from './components/AIConversation/createAIConversation.mjs';
|
|
2
2
|
export { AIConversation } from './components/AIConversation/AIConversation.mjs';
|
|
3
|
-
export { AIContextProvider } from './hooks/AIContextProvider.mjs';
|
|
4
3
|
export { createAIHooks } from './hooks/createAIHooks.mjs';
|