@gooddata/sdk-ui-gen-ai 11.35.0-alpha.6 → 11.35.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.
Files changed (79) hide show
  1. package/esm/components/GenAIChatConversations.js +12 -9
  2. package/esm/components/Input.js +8 -3
  3. package/esm/localization/bundles/de-DE.localization-bundle.d.ts +6 -1
  4. package/esm/localization/bundles/de-DE.localization-bundle.js +6 -1
  5. package/esm/localization/bundles/en-AU.localization-bundle.d.ts +6 -1
  6. package/esm/localization/bundles/en-AU.localization-bundle.js +6 -1
  7. package/esm/localization/bundles/en-GB.localization-bundle.d.ts +6 -1
  8. package/esm/localization/bundles/en-GB.localization-bundle.js +6 -1
  9. package/esm/localization/bundles/es-419.localization-bundle.d.ts +6 -1
  10. package/esm/localization/bundles/es-419.localization-bundle.js +6 -1
  11. package/esm/localization/bundles/es-ES.localization-bundle.d.ts +6 -1
  12. package/esm/localization/bundles/es-ES.localization-bundle.js +6 -1
  13. package/esm/localization/bundles/fi-FI.localization-bundle.d.ts +6 -1
  14. package/esm/localization/bundles/fi-FI.localization-bundle.js +6 -1
  15. package/esm/localization/bundles/fr-CA.localization-bundle.d.ts +6 -1
  16. package/esm/localization/bundles/fr-CA.localization-bundle.js +6 -1
  17. package/esm/localization/bundles/fr-FR.localization-bundle.d.ts +6 -1
  18. package/esm/localization/bundles/fr-FR.localization-bundle.js +6 -1
  19. package/esm/localization/bundles/id-ID.localization-bundle.d.ts +6 -1
  20. package/esm/localization/bundles/id-ID.localization-bundle.js +6 -1
  21. package/esm/localization/bundles/it-IT.localization-bundle.d.ts +6 -1
  22. package/esm/localization/bundles/it-IT.localization-bundle.js +6 -1
  23. package/esm/localization/bundles/ja-JP.localization-bundle.d.ts +6 -1
  24. package/esm/localization/bundles/ja-JP.localization-bundle.js +6 -1
  25. package/esm/localization/bundles/ko-KR.localization-bundle.d.ts +6 -1
  26. package/esm/localization/bundles/ko-KR.localization-bundle.js +6 -1
  27. package/esm/localization/bundles/nl-NL.localization-bundle.d.ts +6 -1
  28. package/esm/localization/bundles/nl-NL.localization-bundle.js +6 -1
  29. package/esm/localization/bundles/pl-PL.localization-bundle.d.ts +6 -1
  30. package/esm/localization/bundles/pl-PL.localization-bundle.js +6 -1
  31. package/esm/localization/bundles/pt-BR.localization-bundle.d.ts +6 -1
  32. package/esm/localization/bundles/pt-BR.localization-bundle.js +6 -1
  33. package/esm/localization/bundles/pt-PT.localization-bundle.d.ts +6 -1
  34. package/esm/localization/bundles/pt-PT.localization-bundle.js +6 -1
  35. package/esm/localization/bundles/ru-RU.localization-bundle.d.ts +6 -1
  36. package/esm/localization/bundles/ru-RU.localization-bundle.js +6 -1
  37. package/esm/localization/bundles/sl-SI.localization-bundle.d.ts +6 -1
  38. package/esm/localization/bundles/sl-SI.localization-bundle.js +6 -1
  39. package/esm/localization/bundles/th-TH.localization-bundle.d.ts +6 -1
  40. package/esm/localization/bundles/th-TH.localization-bundle.js +6 -1
  41. package/esm/localization/bundles/tr-TR.localization-bundle.d.ts +6 -1
  42. package/esm/localization/bundles/tr-TR.localization-bundle.js +6 -1
  43. package/esm/localization/bundles/uk-UA.localization-bundle.d.ts +6 -1
  44. package/esm/localization/bundles/uk-UA.localization-bundle.js +6 -1
  45. package/esm/localization/bundles/vi-VN.localization-bundle.d.ts +6 -1
  46. package/esm/localization/bundles/vi-VN.localization-bundle.js +6 -1
  47. package/esm/localization/bundles/zh-HK.localization-bundle.d.ts +6 -1
  48. package/esm/localization/bundles/zh-HK.localization-bundle.js +6 -1
  49. package/esm/localization/bundles/zh-Hans.localization-bundle.d.ts +6 -1
  50. package/esm/localization/bundles/zh-Hans.localization-bundle.js +6 -1
  51. package/esm/localization/bundles/zh-Hant.localization-bundle.d.ts +6 -1
  52. package/esm/localization/bundles/zh-Hant.localization-bundle.js +6 -1
  53. package/esm/model.d.ts +2 -0
  54. package/esm/store/messages/messagesSelectors.d.ts +3 -3
  55. package/esm/store/messages/messagesSelectors.js +26 -4
  56. package/esm/store/messages/messagesSlice.d.ts +24 -18
  57. package/esm/store/messages/messagesSlice.js +186 -99
  58. package/esm/store/sideEffects/index.js +1 -1
  59. package/esm/store/sideEffects/onConversationDelete.d.ts +2 -0
  60. package/esm/store/sideEffects/onThreadLoad.js +15 -5
  61. package/esm/store/sideEffects/onUserFeedback.d.ts +3 -0
  62. package/esm/store/sideEffects/onUserFeedback.js +2 -1
  63. package/esm/store/sideEffects/onUserMessage.d.ts +9 -2
  64. package/esm/store/sideEffects/onUserMessage.js +42 -22
  65. package/esm/store/sideEffects/onUserMessageUpdate.d.ts +5 -3
  66. package/esm/store/sideEffects/onUserMessageUpdate.js +5 -2
  67. package/esm/store/sideEffects/onVisualisationRender.d.ts +2 -0
  68. package/esm/store/sideEffects/onVisualizationSave.d.ts +4 -0
  69. package/esm/store/sideEffects/onVisualizationSave.js +2 -0
  70. package/esm/store/sideEffects/onVisualizationSuccessSave.d.ts +2 -0
  71. package/esm/store/utils.d.ts +6 -0
  72. package/esm/store/utils.js +30 -0
  73. package/esm/types.d.ts +26 -0
  74. package/package.json +20 -20
  75. package/styles/css/conversations.css +16 -9
  76. package/styles/css/conversations.css.map +1 -1
  77. package/styles/css/main.css +16 -9
  78. package/styles/css/main.css.map +1 -1
  79. package/styles/scss/conversations.scss +20 -9
@@ -2,6 +2,7 @@
2
2
  import { createSlice } from "@reduxjs/toolkit";
3
3
  import { isAssistantMessage, isChatConversationLocalItem, isUserMessage, isVisualizationContents, makeErrorContent, makeErrorContents, } from "../../model.js";
4
4
  import { convertMessageToChatConversation } from "../sideEffects/utils.js";
5
+ import { createEmptyConversation, getConversationData, getConversationLocalId, isConversationWithLocalId, } from "../utils.js";
5
6
  export const LS_VERBOSE_KEY = "gd-gen-ai-verbose";
6
7
  export const messagesSliceName = "messages";
7
8
  /**
@@ -16,17 +17,16 @@ const getInitialVerboseState = () => {
16
17
  };
17
18
  const initialState = {
18
19
  // Start with loading state to avoid re-render from empty state on startup
19
- asyncProcess: "loading",
20
20
  loaded: false,
21
21
  verbose: getInitialVerboseState(),
22
22
  //old messages
23
+ messageAsyncProcess: "loading",
23
24
  messageOrder: [],
24
25
  messages: {},
25
26
  //conversations
26
27
  conversations: undefined,
27
28
  currentConversation: undefined,
28
- conversationItems: {},
29
- conversationItemsOrder: [],
29
+ conversationsData: {},
30
30
  };
31
31
  const setNormalizedMessages = (state, messages) => {
32
32
  state.messages = messages.reduce((acc, message) => {
@@ -41,16 +41,37 @@ const setNormalizedConversations = (state, conversations) => {
41
41
  };
42
42
  const setNormalizedConversation = (state, conversation, items = []) => {
43
43
  state.currentConversation = conversation;
44
- state.conversationItems = items.reduce((acc, message) => {
44
+ state.loaded = true;
45
+ const data = getConversationData(state.conversationsData, conversation.localId);
46
+ const normalizedItems = items.reduce((acc, message) => {
45
47
  acc[message.localId] = message;
46
48
  return acc;
47
49
  }, {});
48
- state.conversationItemsOrder = items.map((message) => message.localId);
49
- state.loaded = true;
50
+ const normalizedOrder = items.map((message) => message.localId);
51
+ if (data) {
52
+ data.order = normalizedOrder;
53
+ data.items = normalizedItems;
54
+ }
55
+ };
56
+ const setConversationInProgress = (state, inProgress, localConversationId) => {
57
+ if (!localConversationId) {
58
+ return;
59
+ }
60
+ state.conversations = state.conversations?.map((conversation) => conversation.localId === localConversationId ? { ...conversation, inProgress } : conversation);
61
+ if (isConversationWithLocalId(state.currentConversation, localConversationId)) {
62
+ state.currentConversation = {
63
+ ...state.currentConversation,
64
+ inProgress,
65
+ };
66
+ }
50
67
  };
51
- const getAssistantMessageStrict = (state, assistantMessageId) => {
68
+ const getAssistantMessageStrict = (state, assistantMessageId, conversationId) => {
52
69
  if (state.currentConversation) {
53
- const message = state.conversationItems[assistantMessageId];
70
+ const data = getConversationData(state.conversationsData, conversationId);
71
+ if (!data) {
72
+ throw new Error(`Unexpected error during message evaluation.`);
73
+ }
74
+ const message = data.items[assistantMessageId];
54
75
  if (message.role !== "assistant") {
55
76
  throw new Error(`Unexpected error during message evaluation.`);
56
77
  }
@@ -64,9 +85,13 @@ const getAssistantMessageStrict = (state, assistantMessageId) => {
64
85
  return message;
65
86
  }
66
87
  };
67
- const getUserMessageStrict = (state, assistantMessageId) => {
88
+ const getUserMessageStrict = (state, assistantMessageId, conversationId) => {
68
89
  if (state.currentConversation) {
69
- const message = state.conversationItems[assistantMessageId];
90
+ const data = getConversationData(state.conversationsData, conversationId);
91
+ if (!data) {
92
+ throw new Error(`Unexpected error during message evaluation.`);
93
+ }
94
+ const message = data.items[assistantMessageId];
70
95
  if (message.role !== "user") {
71
96
  throw new Error(`Unexpected error during message evaluation.`);
72
97
  }
@@ -80,9 +105,13 @@ const getUserMessageStrict = (state, assistantMessageId) => {
80
105
  return message;
81
106
  }
82
107
  };
83
- const getMessageExists = (state, assistantMessageId) => {
108
+ const getMessageExists = (state, assistantMessageId, conversationId) => {
84
109
  if (state.currentConversation) {
85
- return !!state.conversationItems[assistantMessageId];
110
+ if (!conversationId) {
111
+ return false;
112
+ }
113
+ const data = getConversationData(state.conversationsData, conversationId);
114
+ return !!data?.items[assistantMessageId];
86
115
  }
87
116
  return !!state.messages[assistantMessageId];
88
117
  };
@@ -90,13 +119,17 @@ const getMessageExists = (state, assistantMessageId) => {
90
119
  * Get the user message before the assistant message, or undefined if there isn't one.
91
120
  * This is useful for dynamically created assistant messages that don't have a preceding user message.
92
121
  */
93
- const getUserMessageBeforeSafe = (state, assistantMessageId) => {
122
+ const getUserMessageBeforeSafe = (state, assistantMessageId, conversationId) => {
94
123
  if (state.currentConversation) {
95
- const messageIndex = state.conversationItemsOrder.indexOf(assistantMessageId);
124
+ const data = getConversationData(state.conversationsData, conversationId);
125
+ if (!data) {
126
+ return undefined;
127
+ }
128
+ const messageIndex = data.order.indexOf(assistantMessageId);
96
129
  if (messageIndex <= 0) {
97
130
  return undefined;
98
131
  }
99
- const message = state.conversationItems[state.conversationItemsOrder[messageIndex - 1]];
132
+ const message = data.items[data.order[messageIndex - 1]];
100
133
  return message.role === "user" ? message : undefined;
101
134
  }
102
135
  const messageIndex = state.messageOrder.indexOf(assistantMessageId);
@@ -111,20 +144,36 @@ const messagesSlice = createSlice({
111
144
  initialState,
112
145
  reducers: {
113
146
  loadThreadAction: (state) => {
114
- // do not override loading if new conversation landing is active
115
- if (state.currentConversation === "new") {
116
- return;
147
+ if (state.currentConversation) {
148
+ const data = getConversationData(state.conversationsData, state.currentConversation.localId);
149
+ if (data) {
150
+ data.asyncProcess = "loading";
151
+ }
152
+ }
153
+ else {
154
+ state.messageAsyncProcess = "loading";
117
155
  }
118
- state.asyncProcess = "loading";
119
156
  },
120
157
  loadThreadErrorAction: (state, { payload: { error } }) => {
121
158
  state.globalError = errorToObject(error);
122
- delete state.asyncProcess;
159
+ if (state.currentConversation) {
160
+ const data = getConversationData(state.conversationsData, state.currentConversation.localId);
161
+ delete data?.asyncProcess;
162
+ }
163
+ else {
164
+ delete state.messageAsyncProcess;
165
+ }
123
166
  },
124
167
  loadThreadSuccessAction: (state, { payload: { messages, threadId } }) => {
125
168
  setNormalizedMessages(state, messages);
126
169
  state.threadId = threadId;
127
- delete state.asyncProcess;
170
+ if (state.currentConversation) {
171
+ const data = getConversationData(state.conversationsData, state.currentConversation.localId);
172
+ delete data?.asyncProcess;
173
+ }
174
+ else {
175
+ delete state.messageAsyncProcess;
176
+ }
128
177
  },
129
178
  /**
130
179
  * Restore previously cached messages immediately while the backend is still loading.
@@ -135,14 +184,12 @@ const messagesSlice = createSlice({
135
184
  * - The input remains disabled (Input checks !!asyncProcess, and "restoring" is truthy).
136
185
  */
137
186
  restoreCachedMessagesAction: (state, { payload: { messages } }) => {
138
- //TODO: s.hacker New needs to be changed
139
- const normalized = messages.reduce((acc, message) => {
187
+ state.messages = messages.reduce((acc, message) => {
140
188
  acc[message.localId] = message;
141
189
  return acc;
142
190
  }, {});
143
- state.messages = normalized;
144
191
  state.messageOrder = messages.map((message) => message.localId);
145
- state.asyncProcess = "restoring";
192
+ state.messageAsyncProcess = "restoring";
146
193
  },
147
194
  loadConversationsSuccessAction: (state, { payload: { conversations }, }) => {
148
195
  setNormalizedConversations(state, conversations);
@@ -150,31 +197,56 @@ const messagesSlice = createSlice({
150
197
  loadConversationSuccessAction: (state, { payload: { currentConversation, conversationItems, threadId }, }) => {
151
198
  setNormalizedConversation(state, currentConversation, conversationItems);
152
199
  state.threadId = threadId;
153
- delete state.asyncProcess;
200
+ if (state.currentConversation) {
201
+ const data = getConversationData(state.conversationsData, state.currentConversation.localId);
202
+ delete data?.asyncProcess;
203
+ }
204
+ else {
205
+ delete state.messageAsyncProcess;
206
+ }
154
207
  },
155
208
  clearThreadAction: (state) => {
156
- state.asyncProcess = "clearing";
209
+ if (state.currentConversation) {
210
+ const data = getConversationData(state.conversationsData, state.currentConversation.localId);
211
+ if (data) {
212
+ data.asyncProcess = "clearing";
213
+ }
214
+ }
215
+ else {
216
+ state.messageAsyncProcess = "clearing";
217
+ }
157
218
  },
158
219
  clearThreadErrorAction: (state, { payload: { error } }) => {
159
220
  state.globalError = errorToObject(error);
160
- delete state.asyncProcess;
221
+ if (state.currentConversation) {
222
+ const data = getConversationData(state.conversationsData, state.currentConversation.localId);
223
+ delete data?.asyncProcess;
224
+ }
225
+ else {
226
+ delete state.messageAsyncProcess;
227
+ }
161
228
  },
162
229
  clearThreadSuccessAction: (state) => {
163
230
  state.messages = {};
164
231
  state.messageOrder = [];
165
232
  state.loaded = false;
166
- delete state.asyncProcess;
167
233
  delete state.globalError;
234
+ if (state.currentConversation) {
235
+ const data = getConversationData(state.conversationsData, state.currentConversation.localId);
236
+ delete data?.asyncProcess;
237
+ }
238
+ else {
239
+ delete state.messageAsyncProcess;
240
+ }
168
241
  },
169
242
  clearConversationSuccessAction: (state, { payload: { conversation, threadId }, }) => {
170
243
  state.conversations = [conversation, ...(state.conversations ?? [])];
171
244
  state.currentConversation = conversation;
172
- state.conversationItems = {};
173
- state.conversationItemsOrder = [];
174
245
  state.threadId = threadId;
175
246
  state.loaded = false;
176
- delete state.asyncProcess;
177
247
  delete state.globalError;
248
+ const data = getConversationData(state.conversationsData, state.currentConversation.localId);
249
+ delete data?.asyncProcess;
178
250
  },
179
251
  /**
180
252
  * Add message to the stack
@@ -189,20 +261,21 @@ const messagesSlice = createSlice({
189
261
  throw new Error("Working with conversation message but thread mode is active.");
190
262
  }
191
263
  const currentConversation = state.currentConversation;
192
- if (currentConversation !== "new") {
193
- currentConversation.updatedAt = new Date().toISOString();
194
- state.conversations = state.conversations?.map((conversation) => {
195
- if (conversation.id === currentConversation.id) {
196
- return {
197
- ...conversation,
198
- updatedAt: currentConversation.updatedAt,
199
- };
200
- }
201
- return conversation;
202
- });
264
+ currentConversation.updatedAt = new Date().toISOString();
265
+ state.conversations = state.conversations?.map((conversation) => {
266
+ if (conversation.localId === currentConversation.localId) {
267
+ return {
268
+ ...conversation,
269
+ updatedAt: currentConversation.updatedAt,
270
+ };
271
+ }
272
+ return conversation;
273
+ });
274
+ const data = getConversationData(state.conversationsData, currentConversation.localId);
275
+ if (data) {
276
+ data.items[message.localId] = message;
277
+ data.order.push(message.localId);
203
278
  }
204
- state.conversationItems[message.localId] = message;
205
- state.conversationItemsOrder.push(message.localId);
206
279
  }
207
280
  else {
208
281
  if (state.currentConversation) {
@@ -216,13 +289,17 @@ const messagesSlice = createSlice({
216
289
  /**
217
290
  * Start the message evaluation, adding new assistant message as an incomplete placeholder
218
291
  */
219
- evaluateMessageAction: (state, { payload: { message }, }) => {
292
+ evaluateMessageAction: (state, { payload: { message, conversationId }, }) => {
220
293
  if (isChatConversationLocalItem(message)) {
221
- if (!state.currentConversation) {
294
+ if (!conversationId) {
222
295
  throw new Error("Working with conversation message but thread mode is active.");
223
296
  }
224
- state.conversationItems[message.localId] = message;
225
- state.conversationItemsOrder.push(message.localId);
297
+ const data = getConversationData(state.conversationsData, conversationId);
298
+ if (data) {
299
+ data.items[message.localId] = message;
300
+ data.order.push(message.localId);
301
+ }
302
+ setConversationInProgress(state, true, conversationId);
226
303
  }
227
304
  else {
228
305
  if (state.currentConversation) {
@@ -231,23 +308,33 @@ const messagesSlice = createSlice({
231
308
  state.messages[message.localId] = message;
232
309
  state.messageOrder.push(message.localId);
233
310
  }
234
- state.asyncProcess = "evaluating";
311
+ if (state.currentConversation) {
312
+ const data = getConversationData(state.conversationsData, conversationId);
313
+ if (data) {
314
+ data.asyncProcess = "evaluating";
315
+ }
316
+ }
317
+ else {
318
+ state.messageAsyncProcess = "evaluating";
319
+ }
235
320
  },
236
321
  /**
237
322
  * The evaluation failed, need to update the assistant message.
238
323
  */
239
324
  evaluateMessageErrorAction: (state, { payload, }) => {
240
- const assistantMessage = getAssistantMessageStrict(state, payload.assistantMessageId);
325
+ const assistantMessage = getAssistantMessageStrict(state, payload.assistantMessageId, payload.conversationId);
241
326
  if (isChatConversationLocalItem(assistantMessage)) {
242
327
  assistantMessage.complete = true;
243
328
  assistantMessage.streaming = false;
244
329
  assistantMessage.content = makeErrorContent(payload.error);
330
+ const data = getConversationData(state.conversationsData, payload.conversationId);
331
+ delete data?.asyncProcess;
245
332
  }
246
333
  else {
247
334
  assistantMessage.complete = true;
248
335
  assistantMessage.content.push(makeErrorContents(payload.error));
336
+ delete state.messageAsyncProcess;
249
337
  }
250
- delete state.asyncProcess;
251
338
  },
252
339
  /**
253
340
  * Received new chunk from server over SSE.
@@ -255,12 +342,12 @@ const messagesSlice = createSlice({
255
342
  evaluateMessageStreamingAction: (state, { payload, }) => {
256
343
  //NOTE: During streaming a message, user can choose to close or reset a chat
257
344
  // and without this check, we would get an unwanted error
258
- const exists = getMessageExists(state, payload.assistantMessageId);
345
+ const exists = getMessageExists(state, payload.assistantMessageId, payload.conversationId);
259
346
  if (!exists) {
260
347
  return;
261
348
  }
262
349
  // Update assistant message
263
- const assistantMessage = getAssistantMessageStrict(state, payload.assistantMessageId);
350
+ const assistantMessage = getAssistantMessageStrict(state, payload.assistantMessageId, payload.conversationId);
264
351
  assistantMessage.id = payload.interactionId ?? assistantMessage.id;
265
352
  if (isChatConversationLocalItem(assistantMessage)) {
266
353
  assistantMessage.content = payload.content ?? {
@@ -279,21 +366,20 @@ const messagesSlice = createSlice({
279
366
  // Also update the interaction id in the relevant user message (if one exists)
280
367
  // Note: dynamically created assistant messages (for multi-interaction streams)
281
368
  // may not have a preceding user message
282
- const userMessage = getUserMessageBeforeSafe(state, payload.assistantMessageId);
369
+ const userMessage = getUserMessageBeforeSafe(state, payload.assistantMessageId, payload.conversationId);
283
370
  if (userMessage) {
284
371
  userMessage.id = payload.interactionId ?? userMessage.id;
285
372
  }
286
373
  },
287
374
  evaluateConversationTitleAction: (state, { payload, }) => {
288
- const conversation = state.conversations?.find((c) => c.id === payload.conversation.id);
375
+ const conversation = state.conversations?.find((c) => c.localId === payload.conversation.localId);
289
376
  if (!conversation) {
290
377
  return;
291
378
  }
292
379
  conversation.generatingTitle = payload.generatingTitle;
293
380
  if (payload.title) {
294
381
  conversation.title = payload.title;
295
- if (state.currentConversation !== "new" &&
296
- state.currentConversation?.id === conversation.id) {
382
+ if (isConversationWithLocalId(state.currentConversation, conversation.localId)) {
297
383
  state.currentConversation.title = payload.title;
298
384
  }
299
385
  }
@@ -301,12 +387,12 @@ const messagesSlice = createSlice({
301
387
  evaluateMessageUpdateAction: (state, { payload, }) => {
302
388
  //NOTE: During streaming a message, user can choose to close or reset a chat
303
389
  // and without this check, we would get an unwanted error
304
- const exists = getMessageExists(state, payload.userMessageId);
390
+ const exists = getMessageExists(state, payload.userMessageId, payload.conversationId);
305
391
  if (!exists) {
306
392
  return;
307
393
  }
308
394
  // Update assistant message
309
- const userMessage = getUserMessageStrict(state, payload.userMessageId);
395
+ const userMessage = getUserMessageStrict(state, payload.userMessageId, payload.conversationId);
310
396
  userMessage.id = payload.interactionId ?? userMessage.id;
311
397
  if (isChatConversationLocalItem(userMessage)) {
312
398
  const message = payload.message;
@@ -320,12 +406,15 @@ const messagesSlice = createSlice({
320
406
  }
321
407
  },
322
408
  evaluateMessageCompleteAction: (state, { payload, }) => {
323
- const assistantMessage = getAssistantMessageStrict(state, payload.assistantMessageId);
409
+ const assistantMessage = getAssistantMessageStrict(state, payload.assistantMessageId, payload.conversationId);
324
410
  assistantMessage.complete = true;
325
- delete state.asyncProcess;
411
+ setConversationInProgress(state, false, payload.conversationId);
412
+ const data = getConversationData(state.conversationsData, payload.conversationId);
413
+ delete data?.asyncProcess;
414
+ delete state.messageAsyncProcess;
326
415
  },
327
416
  evaluateMessageSuggestionsAction: (state, { payload, }) => {
328
- const assistantMessage = getAssistantMessageStrict(state, payload.assistantMessageId);
417
+ const assistantMessage = getAssistantMessageStrict(state, payload.assistantMessageId, payload.conversationId);
329
418
  if (isChatConversationLocalItem(assistantMessage)) {
330
419
  assistantMessage.suggestions = payload.item;
331
420
  }
@@ -344,31 +433,23 @@ const messagesSlice = createSlice({
344
433
  },
345
434
  startNewConversationAction: (state) => {
346
435
  // keep list of conversations, but clear current conversation data in the view
347
- state.currentConversation = "new";
348
- state.conversationItems = {};
349
- state.conversationItemsOrder = [];
436
+ state.currentConversation = createEmptyConversation();
350
437
  // mark as loaded to prevent auto-loading last conversation while on landing
351
438
  state.loaded = true;
352
- delete state.asyncProcess;
353
439
  },
354
440
  setCurrentConversationAction: (state, { payload }) => {
355
- const existing = state.conversations?.find((c) => c.id === payload.conversation.id);
441
+ const existing = state.conversations?.find((c) => c.localId === payload.conversation.localId);
356
442
  if (existing) {
357
- //Same conversation as current, do nothing
358
- if (state.currentConversation !== "new" && state.currentConversation?.id === existing.id) {
359
- return;
360
- }
443
+ const data = getConversationData(state.conversationsData, existing.localId);
361
444
  state.currentConversation = existing;
362
- state.threadId = existing.id;
363
- state.messages = {};
364
- state.messageOrder = [];
365
- state.loaded = false;
366
- //TODO: Existing conversation
445
+ state.loaded = !!data?.order.length;
446
+ state.threadId = payload.conversation.id;
447
+ existing.id = payload.conversation.id;
367
448
  }
368
449
  else {
369
450
  state.conversations = [payload.conversation, ...(state.conversations ?? [])];
370
451
  state.currentConversation = payload.conversation;
371
- state.threadId = payload.conversation.id;
452
+ state.threadId = payload.conversation.localId;
372
453
  state.loaded = true;
373
454
  }
374
455
  },
@@ -376,10 +457,17 @@ const messagesSlice = createSlice({
376
457
  state.globalError = errorToObject(error);
377
458
  },
378
459
  cancelAsyncAction: (state) => {
379
- delete state.asyncProcess;
460
+ if (state.currentConversation) {
461
+ const data = getConversationData(state.conversationsData, state.currentConversation.localId);
462
+ delete data?.asyncProcess;
463
+ }
464
+ else {
465
+ delete state.messageAsyncProcess;
466
+ }
380
467
  },
381
468
  setUserFeedback: (state, { payload, }) => {
382
- const assistantMessage = getAssistantMessageStrict(state, payload.assistantMessageId);
469
+ const conversationId = getConversationLocalId(state.currentConversation);
470
+ const assistantMessage = getAssistantMessageStrict(state, payload.assistantMessageId, conversationId);
383
471
  if (isChatConversationLocalItem(assistantMessage)) {
384
472
  const original = assistantMessage.feedback ?? {
385
473
  type: "feedback",
@@ -397,7 +485,7 @@ const messagesSlice = createSlice({
397
485
  },
398
486
  setUserFeedbackError: (state, { payload, }) => {
399
487
  // Reset feedback to NONE and set error when submission fails
400
- const assistantMessage = getAssistantMessageStrict(state, payload.assistantMessageId);
488
+ const assistantMessage = getAssistantMessageStrict(state, payload.assistantMessageId, payload.conversationId);
401
489
  if (isChatConversationLocalItem(assistantMessage)) {
402
490
  const original = assistantMessage.feedback ?? {
403
491
  type: "feedback",
@@ -417,7 +505,7 @@ const messagesSlice = createSlice({
417
505
  },
418
506
  clearUserFeedbackError: (state, { payload, }) => {
419
507
  // Clear feedback error after showing toast
420
- const assistantMessage = getAssistantMessageStrict(state, payload.assistantMessageId);
508
+ const assistantMessage = getAssistantMessageStrict(state, payload.assistantMessageId, payload.conversationId);
421
509
  if (isChatConversationLocalItem(assistantMessage)) {
422
510
  delete assistantMessage.feedback?.error;
423
511
  }
@@ -426,7 +514,8 @@ const messagesSlice = createSlice({
426
514
  }
427
515
  },
428
516
  saveVisualizationAction: (state, { payload, }) => {
429
- const assistantMessage = getAssistantMessageStrict(state, payload.assistantMessageId);
517
+ const conversationId = getConversationLocalId(state.currentConversation);
518
+ const assistantMessage = getAssistantMessageStrict(state, payload.assistantMessageId, conversationId);
430
519
  if (isChatConversationLocalItem(assistantMessage)) {
431
520
  if (assistantMessage.content.type !== "multipart") {
432
521
  throw new Error("Unexpected message type");
@@ -453,7 +542,8 @@ const messagesSlice = createSlice({
453
542
  }
454
543
  },
455
544
  savedVisualizationAction: (state, { payload, }) => {
456
- const assistantMessage = getAssistantMessageStrict(state, payload.assistantMessageId);
545
+ const conversationId = getConversationLocalId(state.currentConversation);
546
+ const assistantMessage = getAssistantMessageStrict(state, payload.assistantMessageId, conversationId);
457
547
  if (isChatConversationLocalItem(assistantMessage)) {
458
548
  if (assistantMessage.content.type !== "multipart") {
459
549
  throw new Error("Unexpected message type");
@@ -467,7 +557,7 @@ const messagesSlice = createSlice({
467
557
  }
468
558
  },
469
559
  saveVisualizationErrorAction: (state, { payload, }) => {
470
- const assistantMessage = getAssistantMessageStrict(state, payload.assistantMessageId);
560
+ const assistantMessage = getAssistantMessageStrict(state, payload.assistantMessageId, payload.conversationId);
471
561
  if (isChatConversationLocalItem(assistantMessage)) {
472
562
  if (assistantMessage.content.type !== "multipart") {
473
563
  throw new Error("Unexpected message type");
@@ -494,7 +584,7 @@ const messagesSlice = createSlice({
494
584
  }
495
585
  },
496
586
  saveVisualizationSuccessAction: (state, { payload, }) => {
497
- const assistantMessage = getAssistantMessageStrict(state, payload.assistantMessageId);
587
+ const assistantMessage = getAssistantMessageStrict(state, payload.assistantMessageId, payload.conversationId);
498
588
  if (isChatConversationLocalItem(assistantMessage)) {
499
589
  if (assistantMessage.content.type !== "multipart") {
500
590
  throw new Error("Unexpected message type");
@@ -523,7 +613,8 @@ const messagesSlice = createSlice({
523
613
  },
524
614
  saveVisualisationRenderStatusAction: (state, _action) => state,
525
615
  saveVisualisationRenderStatusSuccessAction: (state, { payload, }) => {
526
- const assistantMessage = getAssistantMessageStrict(state, payload.assistantMessageId);
616
+ const conversationId = getConversationLocalId(state.currentConversation);
617
+ const assistantMessage = getAssistantMessageStrict(state, payload.assistantMessageId, conversationId);
527
618
  if (isChatConversationLocalItem(assistantMessage)) {
528
619
  if (assistantMessage.content.type !== "multipart") {
529
620
  throw new Error("Unexpected message type");
@@ -548,14 +639,13 @@ const messagesSlice = createSlice({
548
639
  visualizationErrorAction: (state, _action) => state,
549
640
  pinConversationAction: (state, _action) => state,
550
641
  pinConversationSuccessAction: (state, { payload }) => {
551
- state.conversations = state.conversations?.map((conversation) => conversation.id === payload.conversationId
642
+ state.conversations = state.conversations?.map((conversation) => conversation.localId === payload.conversationId
552
643
  ? {
553
644
  ...conversation,
554
645
  pinned: payload.pinned,
555
646
  }
556
647
  : conversation);
557
- if (state.currentConversation !== "new" &&
558
- state.currentConversation?.id === payload.conversationId) {
648
+ if (isConversationWithLocalId(state.currentConversation, payload.conversationId)) {
559
649
  state.currentConversation = {
560
650
  ...state.currentConversation,
561
651
  pinned: payload.pinned,
@@ -563,14 +653,13 @@ const messagesSlice = createSlice({
563
653
  }
564
654
  },
565
655
  pinConversationFailureAction: (state, { payload }) => {
566
- state.conversations = state.conversations?.map((conversation) => conversation.id === payload.conversationId
656
+ state.conversations = state.conversations?.map((conversation) => conversation.localId === payload.conversationId
567
657
  ? {
568
658
  ...conversation,
569
659
  pinned: payload.pinned,
570
660
  }
571
661
  : conversation);
572
- if (state.currentConversation !== "new" &&
573
- state.currentConversation?.id === payload.conversationId) {
662
+ if (isConversationWithLocalId(state.currentConversation, payload.conversationId)) {
574
663
  state.currentConversation = {
575
664
  ...state.currentConversation,
576
665
  pinned: payload.pinned,
@@ -579,14 +668,12 @@ const messagesSlice = createSlice({
579
668
  },
580
669
  deleteConversationAction: (state, _action) => state,
581
670
  deleteConversationStartAction: (state, { payload }) => {
582
- if (state.currentConversation !== "new" &&
583
- payload.conversationId === state.currentConversation?.id) {
671
+ if (isConversationWithLocalId(state.currentConversation, payload.conversationId)) {
584
672
  // keep list of conversations, but clear current conversation data in the view
585
- state.currentConversation = "new";
586
- state.conversationItems = {};
587
- state.conversationItemsOrder = [];
673
+ state.currentConversation = createEmptyConversation();
588
674
  }
589
- state.conversations = state.conversations?.filter((conversation) => conversation.id !== payload.conversationId);
675
+ delete state.conversationsData[payload.conversationId];
676
+ state.conversations = state.conversations?.filter((conversation) => conversation.localId !== payload.conversationId);
590
677
  },
591
678
  deleteConversationSuccessAction: (state, _action) => state,
592
679
  deleteConversationFailureAction: (state, { payload }) => {
@@ -23,7 +23,7 @@ import { onVisualizationSuccessSave } from "./onVisualizationSuccessSave.js";
23
23
  export function* rootSaga() {
24
24
  yield takeLatest(loadThreadAction.type, onThreadLoad);
25
25
  yield takeLatest(clearThreadAction.type, onThreadClear);
26
- yield takeLatest(newMessageAction.type, onUserMessage);
26
+ yield takeEvery(newMessageAction.type, onUserMessage);
27
27
  //messages API
28
28
  yield takeEvery(setUserFeedback.type, onUserFeedback);
29
29
  yield takeEvery(saveVisualizationAction.type, onVisualizationSave);
@@ -20,5 +20,7 @@ export declare function onConversationDelete({ payload }: PayloadAction<{
20
20
  };
21
21
  type: "messages/deleteConversationFailureAction";
22
22
  }> | import("redux-saga/effects").SelectEffect, void, IAnalyticalBackend & string & import("@gooddata/sdk-backend-spi").IChatConversation & {
23
+ localId: string;
23
24
  generatingTitle?: boolean | undefined;
25
+ inProgress?: boolean | undefined;
24
26
  }>;
@@ -5,6 +5,7 @@ import { settingsSelector } from "../chatWindow/chatWindowSelectors.js";
5
5
  import { loadMessages } from "../localStorage.js";
6
6
  import { conversationSelector, conversationsSelector } from "../messages/messagesSelectors.js";
7
7
  import { cancelAsyncAction, loadConversationSuccessAction, loadConversationsSuccessAction, loadThreadErrorAction, loadThreadSuccessAction, restoreCachedMessagesAction, } from "../messages/messagesSlice.js";
8
+ import { createEmptyConversation } from "../utils.js";
8
9
  import { interactionsToMessages } from "./converters/interactionsToMessages.js";
9
10
  import { convertToLocalContent } from "./converters/toLocalContent.js";
10
11
  /**
@@ -124,7 +125,12 @@ function* fetchAllConversations() {
124
125
  if (cancelledConversations) {
125
126
  return;
126
127
  }
127
- const conversationItems = resultsConversations.items;
128
+ const conversationItems = resultsConversations.items.map((item) => {
129
+ return {
130
+ ...item,
131
+ localId: item.id,
132
+ };
133
+ });
128
134
  yield put(loadConversationsSuccessAction({
129
135
  conversations: conversationItems,
130
136
  }));
@@ -136,16 +142,20 @@ function* fetchCurrentConversation() {
136
142
  const conversations = yield select(conversationsSelector);
137
143
  const conversation = yield select(conversationSelector);
138
144
  // New conversation selected
139
- if (conversation === "new") {
145
+ if (conversation?.localId && !conversation.id) {
140
146
  yield put(loadConversationSuccessAction({
141
- currentConversation: "new",
147
+ currentConversation: conversation,
142
148
  conversationItems: [],
143
149
  }));
144
150
  return;
145
151
  }
146
152
  const api = backend.workspace(workspace).genAI().getChatConversations({ isPreview });
153
+ //Sort by createion date, because normally there are pinned on top
154
+ const sortedConv = conversations
155
+ ?.slice()
156
+ .sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime());
147
157
  //Load the first conversation if there are any or create a new one
148
- const selectedConversation = conversation ?? conversations?.[0] ?? undefined;
158
+ const selectedConversation = conversation ?? sortedConv?.[0] ?? undefined;
149
159
  if (selectedConversation) {
150
160
  const preparedThread = api.getConversationThread(selectedConversation.id);
151
161
  const [resultsItems, cancelledItems] = yield race([call(fetchConversationHistory, preparedThread), take(cancelAsyncAction.type)]);
@@ -166,7 +176,7 @@ function* fetchCurrentConversation() {
166
176
  }
167
177
  else {
168
178
  yield put(loadConversationSuccessAction({
169
- currentConversation: "new",
179
+ currentConversation: createEmptyConversation(),
170
180
  conversationItems: [],
171
181
  }));
172
182
  }