@truedat/ai 8.5.4 → 8.5.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truedat/ai",
3
- "version": "8.5.4",
3
+ "version": "8.5.6",
4
4
  "description": "Truedat Web Artificial Intelligence package",
5
5
  "sideEffects": false,
6
6
  "module": "src/index.js",
@@ -50,7 +50,7 @@
50
50
  "@testing-library/jest-dom": "^6.6.3",
51
51
  "@testing-library/react": "^16.3.0",
52
52
  "@testing-library/user-event": "^14.6.1",
53
- "@truedat/test": "8.5.4",
53
+ "@truedat/test": "8.5.6",
54
54
  "identity-obj-proxy": "^3.0.0",
55
55
  "jest": "^29.7.0",
56
56
  "redux-saga-test-plan": "^4.0.6"
@@ -80,5 +80,5 @@
80
80
  "semantic-ui-react": "^3.0.0-beta.2",
81
81
  "swr": "^2.3.3"
82
82
  },
83
- "gitHead": "75272567eb3ec948a5cdeb8346ef9cacac58267f"
83
+ "gitHead": "41e5e6138f5622558bae4151e720c040c4581162"
84
84
  }
package/src/api.js CHANGED
@@ -16,12 +16,14 @@ const API_SUGGESTIONS_REQUEST = "/api/suggestions/request";
16
16
  const API_TRANSLATIONS_AVAILABILITY_CHECK =
17
17
  "/api/translations/availability_check";
18
18
  const API_TRANSLATIONS_REQUEST = "/api/translations/request";
19
+ const API_AGENT_LAYER_CONVERSATIONS = "/api/agent_layer/conversations";
19
20
  const API_AGENT_LAYER_CONVERSATION = "/api/agent_layer/conversation";
20
21
  const API_AGENT_LAYER_RUN = "/api/agent_layer/run";
21
22
  const API_SOCKET_ENDPOINT = "/api/socket";
22
23
 
23
24
  export {
24
25
  API_ACTION,
26
+ API_AGENT_LAYER_CONVERSATIONS,
25
27
  API_ACTIONS,
26
28
  API_ACTIONS_SEARCH,
27
29
  API_ACTION_SET_ACTIVE,
@@ -10,6 +10,7 @@ import Actions from "./actions/Actions";
10
10
  import Action from "./actions/Action";
11
11
  import ActionEdit from "./actions/ActionEdit";
12
12
  import ActionNew from "./actions/ActionNew";
13
+ import AssistantPage from "./assistant/AssistantPage";
13
14
 
14
15
  export default function AiRoutes() {
15
16
  return (
@@ -52,6 +53,8 @@ export default function AiRoutes() {
52
53
  <Route path={":id"} element={<Action />} />
53
54
  </Route>
54
55
 
56
+ <Route path={"ai-assistant"} element={<AssistantPage />} />
57
+
55
58
  <Route path="*" element={null} />
56
59
  </Routes>
57
60
  );
@@ -1,123 +1,231 @@
1
- import { useRef, useState, useCallback, useEffect } from "react";
1
+ import { useRef, useState, useCallback, useEffect, useMemo } from "react";
2
2
  import { useWebContext } from "@truedat/core/webContext";
3
+ import { Icon } from "semantic-ui-react";
4
+ import { useIntl } from "react-intl";
5
+ import { useNavigate } from "react-router";
6
+ import truedatLogo from "assets/truedat-logo-home-no-text.png";
3
7
 
4
8
  import useAssistantSocket from "./hooks/useAssistantSocket";
5
9
  import useAgentConversation from "./hooks/useAgentConversation";
6
- import AssistantBubble from "./AssistantBubble";
10
+ import useAgentConversations from "../../hooks/useAgentConversations";
7
11
  import AssistantChat from "./AssistantChat";
8
12
 
9
- const FADE_DURATION_MS = 150;
10
13
  const EMPTY_CONVERSATION = [];
14
+ const PANEL_ANIM_MS = 200;
15
+ const AI_ASSISTANT_PATH = "/ai-assistant";
11
16
 
12
17
  const Assistant = () => {
13
- const { disable_td_ai: disableTdAi = false } = useWebContext();
18
+ const {
19
+ disable_td_ai: disableTdAi = false,
20
+ assistant: {
21
+ name: assistantName = null,
22
+ welcome_message: welcomeMessage = null,
23
+ exact = false,
24
+ } = {},
25
+ } = useWebContext();
26
+ const { formatMessage } = useIntl();
27
+ const navigate = useNavigate();
14
28
 
15
29
  const [isOpen, setIsOpen] = useState(false);
16
- const { conversationId, startNewConversation } = useAgentConversation(isOpen);
17
- const [contentFadeOut, setContentFadeOut] = useState(false);
18
- const [contentFadeIn, setContentFadeIn] = useState(false);
19
-
30
+ const [isClosing, setIsClosing] = useState(false);
31
+ const { conversationId, startNewConversation, selectConversation } =
32
+ useAgentConversation(isOpen);
33
+ const { conversations, refresh: refreshConversations } = useAgentConversations();
20
34
  const [conversation, setConversation] = useState(EMPTY_CONVERSATION);
21
- const prevConversationIdRef = useRef(null);
35
+ const [conversationTitle, setConversationTitle] = useState(null);
22
36
 
23
- const containerRef = useRef(null);
37
+ const prevConversationIdRef = useRef(null);
24
38
  const serverMessageHandlerRef = useRef(null);
25
- const pendingTransitionEndRef = useRef(false);
26
- const fadeTimeoutRef = useRef(null);
27
- const fadeInTimeoutRef = useRef(null);
39
+ const triggerRef = useRef(null);
40
+ const panelRef = useRef(null);
41
+ const closeTimerRef = useRef(null);
42
+
43
+ useEffect(() => {
44
+ const handleOpen = (e) => {
45
+ const id = e.detail?.conversationId;
46
+ if (!id) return;
47
+ clearTimeout(closeTimerRef.current);
48
+ setIsClosing(false);
49
+ selectConversation(id);
50
+ setIsOpen(true);
51
+ };
52
+ window.addEventListener("assistant:open", handleOpen);
53
+ return () => window.removeEventListener("assistant:open", handleOpen);
54
+ }, [selectConversation]);
28
55
 
29
56
  useEffect(() => {
30
57
  if (conversationId !== prevConversationIdRef.current) {
31
58
  prevConversationIdRef.current = conversationId;
32
59
  if (conversationId != null) {
33
60
  setConversation(EMPTY_CONVERSATION);
61
+ const fromList = conversations.find(
62
+ (c) => String(c.id) === String(conversationId)
63
+ );
64
+ setConversationTitle(fromList?.title ?? null);
34
65
  }
35
66
  }
36
- }, [conversationId]);
67
+ }, [conversationId, conversations]);
68
+
69
+ useEffect(() => () => clearTimeout(closeTimerRef.current), []);
37
70
 
38
71
  const onChannelJoined = useCallback(() => {}, []);
39
- const handleServerMessage = useCallback((event, payload) => {
40
- serverMessageHandlerRef.current?.(event, payload);
41
- }, []);
72
+ const handleServerMessage = useCallback(
73
+ (event, payload) => {
74
+ if (event === "history") {
75
+ const messages = payload.messages || [];
76
+ const restored = messages
77
+ .filter((m) => m.role === "user" || m.role === "assistant")
78
+ .map((m) =>
79
+ m.role === "user"
80
+ ? { eventType: "request", side: "user", content: m.message }
81
+ : {
82
+ eventType: "response",
83
+ side: "agent",
84
+ content: m.message,
85
+ streaming: false,
86
+ source: "history",
87
+ priority: 0,
88
+ },
89
+ );
90
+ setConversation(restored);
91
+ return;
92
+ }
93
+ if (event === "title") {
94
+ const title = payload?.title;
95
+ if (title) {
96
+ setConversationTitle(title);
97
+ refreshConversations();
98
+ }
99
+ return;
100
+ }
101
+ serverMessageHandlerRef.current?.(event, payload);
102
+ },
103
+ [refreshConversations],
104
+ );
105
+
42
106
  const handleNewChat = useCallback(() => {
43
107
  setConversation(EMPTY_CONVERSATION);
44
108
  startNewConversation();
45
109
  }, [startNewConversation]);
46
110
 
111
+ const handleLoadLastConversation = useCallback(() => {
112
+ if (conversations.length === 0) return;
113
+ selectConversation(conversations[0].id);
114
+ }, [conversations, selectConversation]);
115
+
116
+ const handleMoreConversations = useCallback(() => {
117
+ const isInteracted =
118
+ conversationId != null &&
119
+ conversations.some((c) => String(c.id) === String(conversationId));
120
+ navigate(
121
+ AI_ASSISTANT_PATH,
122
+ isInteracted ? { state: { conversationId } } : undefined
123
+ );
124
+ }, [navigate, conversationId, conversations]);
125
+
126
+ const startClose = useCallback(() => {
127
+ setIsClosing(true);
128
+ closeTimerRef.current = setTimeout(() => {
129
+ setIsOpen(false);
130
+ setIsClosing(false);
131
+ }, PANEL_ANIM_MS);
132
+ }, []);
133
+
134
+ const handleClose = useCallback(() => startClose(), [startClose]);
135
+
136
+ const handleToggle = useCallback(() => {
137
+ if (!isOpen) {
138
+ setIsOpen(true);
139
+ } else if (isClosing) {
140
+ clearTimeout(closeTimerRef.current);
141
+ setIsClosing(false);
142
+ } else {
143
+ startClose();
144
+ }
145
+ }, [isOpen, isClosing, startClose]);
146
+
47
147
  const effectiveConversationId = isOpen ? conversationId : null;
148
+ const currentTitle = useMemo(
149
+ () =>
150
+ conversationTitle ??
151
+ conversations.find((c) => String(c.id) === String(conversationId))?.title ??
152
+ null,
153
+ [conversationTitle, conversationId, conversations]
154
+ );
48
155
  const { socketReady, sendPrompt } = useAssistantSocket(disableTdAi, {
49
156
  conversationId: effectiveConversationId,
50
157
  onChannelJoined,
51
158
  onServerMessage: handleServerMessage,
159
+ assistantName,
160
+ welcomeMessage,
161
+ exact,
52
162
  });
53
163
 
54
- const handleTransitionEnd = useCallback(() => {
55
- if (!pendingTransitionEndRef.current) return;
56
- pendingTransitionEndRef.current = false;
57
- setContentFadeIn(true);
58
- fadeInTimeoutRef.current = setTimeout(() => {
59
- setContentFadeIn(false);
60
- }, FADE_DURATION_MS);
61
- }, []);
62
-
63
164
  useEffect(() => {
64
- const el = containerRef.current;
65
- if (!el) return;
66
- el.addEventListener("transitionend", handleTransitionEnd);
67
- return () => {
68
- el.removeEventListener("transitionend", handleTransitionEnd);
165
+ if (!isOpen) return;
166
+ const handleClickOutside = (e) => {
167
+ if (
168
+ panelRef.current &&
169
+ !panelRef.current.contains(e.target) &&
170
+ triggerRef.current &&
171
+ !triggerRef.current.contains(e.target)
172
+ ) {
173
+ startClose();
174
+ }
69
175
  };
70
- }, [handleTransitionEnd]);
71
-
72
- useEffect(() => {
73
- return () => {
74
- if (fadeTimeoutRef.current) clearTimeout(fadeTimeoutRef.current);
75
- if (fadeInTimeoutRef.current) clearTimeout(fadeInTimeoutRef.current);
76
- };
77
- }, []);
78
-
79
- const startOpenTransition = useCallback(() => {
80
- setContentFadeOut(true);
81
- fadeTimeoutRef.current = setTimeout(() => {
82
- setContentFadeOut(false);
83
- setIsOpen(true);
84
- pendingTransitionEndRef.current = true;
85
- }, FADE_DURATION_MS);
86
- }, []);
87
-
88
- const startCloseTransition = useCallback(() => {
89
- setContentFadeOut(true);
90
- fadeTimeoutRef.current = setTimeout(() => {
91
- setContentFadeOut(false);
92
- setIsOpen(false);
93
- pendingTransitionEndRef.current = true;
94
- }, FADE_DURATION_MS);
95
- }, []);
176
+ document.addEventListener("mousedown", handleClickOutside);
177
+ return () => document.removeEventListener("mousedown", handleClickOutside);
178
+ }, [isOpen, startClose]);
96
179
 
97
180
  if (!socketReady) return null;
98
181
 
182
+ const isLogoSrcString = typeof truedatLogo === "string";
183
+
99
184
  return (
100
- <div
101
- ref={containerRef}
102
- className={`assistant-container assistant-container--${isOpen ? "open" : "closed"}`}
103
- >
104
- {isOpen ? (
105
- <AssistantChat
106
- conversation={conversation}
107
- setConversation={setConversation}
108
- onNewChat={handleNewChat}
109
- onClose={startCloseTransition}
110
- onSendMessage={sendPrompt}
111
- serverMessageHandlerRef={serverMessageHandlerRef}
112
- fadeOut={contentFadeOut}
113
- fadeIn={contentFadeIn}
114
- />
115
- ) : (
116
- <AssistantBubble
117
- onClick={startOpenTransition}
118
- fadeOut={contentFadeOut}
119
- fadeIn={contentFadeIn}
120
- />
185
+ <div className="assistant-container">
186
+ <button
187
+ ref={triggerRef}
188
+ type="button"
189
+ className={[
190
+ "assistant-bubble",
191
+ isOpen && !isClosing && "assistant-bubble--active",
192
+ ]
193
+ .filter(Boolean)
194
+ .join(" ")}
195
+ onClick={handleToggle}
196
+ aria-label={formatMessage({ id: "assistant.bubble.label" })}
197
+ aria-expanded={isOpen && !isClosing}
198
+ >
199
+ {isLogoSrcString ? (
200
+ <img src={truedatLogo} alt="" className="assistant-bubble__logo" />
201
+ ) : (
202
+ <Icon name="comments" />
203
+ )}
204
+ </button>
205
+ {isOpen && (
206
+ <div
207
+ ref={panelRef}
208
+ className={[
209
+ "assistant-panel",
210
+ isClosing && "assistant-panel--closing",
211
+ ]
212
+ .filter(Boolean)
213
+ .join(" ")}
214
+ >
215
+ <AssistantChat
216
+ conversation={conversation}
217
+ setConversation={setConversation}
218
+ onNewChat={handleNewChat}
219
+ onClose={handleClose}
220
+ onSendMessage={sendPrompt}
221
+ onLoadLastConversation={
222
+ conversations.length > 0 ? handleLoadLastConversation : undefined
223
+ }
224
+ onMoreConversations={handleMoreConversations}
225
+ serverMessageHandlerRef={serverMessageHandlerRef}
226
+ title={currentTitle}
227
+ />
228
+ </div>
121
229
  )}
122
230
  </div>
123
231
  );
@@ -42,7 +42,7 @@ function getContentFromPayload(event, payload) {
42
42
  const err = payload.error;
43
43
  const errMsg =
44
44
  err && typeof err === "object"
45
- ? err.message ?? err.reason ?? JSON.stringify(err)
45
+ ? (err.message ?? err.reason ?? JSON.stringify(err))
46
46
  : String(err ?? "");
47
47
  return { content: errMsg, isError: true, priority, source };
48
48
  }
@@ -50,7 +50,7 @@ function getContentFromPayload(event, payload) {
50
50
  const log = payload.content;
51
51
  const logMsg =
52
52
  log && typeof log === "object"
53
- ? log.message ?? log.level ?? JSON.stringify(log)
53
+ ? (log.message ?? log.level ?? JSON.stringify(log))
54
54
  : String(log ?? "");
55
55
  return { content: logMsg, isError: false, priority, source };
56
56
  }
@@ -66,10 +66,7 @@ function appendStreamChunk(prev, source, chunk, priority) {
66
66
  last?.streaming &&
67
67
  last?.source === source;
68
68
  if (sameSource) {
69
- return [
70
- ...prev.slice(0, -1),
71
- { ...last, content: last.content + chunk },
72
- ];
69
+ return [...prev.slice(0, -1), { ...last, content: last.content + chunk }];
73
70
  }
74
71
  return [
75
72
  ...prev,
@@ -105,7 +102,8 @@ function buildRenderItems(messages) {
105
102
  logBuffer.push(msg);
106
103
  } else {
107
104
  if (logBuffer.length > 0) {
108
- const isFollowedByP0 = msg.eventType === "response" && msg.priority === 0;
105
+ const isFollowedByP0 =
106
+ msg.eventType === "response" && msg.priority === 0;
109
107
  result.push({
110
108
  type: "thinking-block",
111
109
  items: logBuffer,
@@ -116,7 +114,11 @@ function buildRenderItems(messages) {
116
114
  logBuffer = [];
117
115
  startIdx = null;
118
116
  }
119
- result.push({ type: "message", msg, key: `${msg.eventType}-${msg.index}` });
117
+ result.push({
118
+ type: "message",
119
+ msg,
120
+ key: `${msg.eventType}-${msg.index}`,
121
+ });
120
122
  }
121
123
  });
122
124
  if (logBuffer.length > 0) {
@@ -137,9 +139,12 @@ const AssistantChat = ({
137
139
  onNewChat,
138
140
  onClose,
139
141
  onSendMessage,
142
+ onLoadLastConversation,
143
+ onMoreConversations,
140
144
  serverMessageHandlerRef,
141
145
  fadeOut,
142
146
  fadeIn,
147
+ title,
143
148
  }) => {
144
149
  const { formatMessage } = useIntl();
145
150
  const [inputValue, setInputValue] = useState("");
@@ -179,15 +184,26 @@ const AssistantChat = ({
179
184
  const ref = serverMessageHandlerRef;
180
185
  if (!ref) return;
181
186
  ref.current = (event, payload) => {
182
- const { content, isError, priority, source } = getContentFromPayload(event, payload);
187
+ const { content, isError, priority, source } = getContentFromPayload(
188
+ event,
189
+ payload,
190
+ );
183
191
  if (event === "stream") {
184
192
  requestAnimationFrame(() => {
185
- setConversation((prev) => appendStreamChunk(prev, source, content, priority));
193
+ setConversation((prev) =>
194
+ appendStreamChunk(prev, source, content, priority),
195
+ );
186
196
  });
187
197
  } else if (event === "log") {
188
198
  setConversation((prev) => [
189
199
  ...prev,
190
- { eventType: "log", side: "agent", content: content || " ", priority, source },
200
+ {
201
+ eventType: "log",
202
+ side: "agent",
203
+ content: content || " ",
204
+ priority,
205
+ source,
206
+ },
191
207
  ]);
192
208
  } else if (event === "error" && Number(priority) === 2) {
193
209
  setConversation((prev) => [
@@ -223,17 +239,17 @@ const AssistantChat = ({
223
239
 
224
240
  const visibleMessages = useMemo(
225
241
  () => getVisibleMessages(conversation),
226
- [conversation]
242
+ [conversation],
227
243
  );
228
244
 
229
245
  const renderItems = useMemo(
230
246
  () => buildRenderItems(visibleMessages),
231
- [visibleMessages]
247
+ [visibleMessages],
232
248
  );
233
249
 
234
250
  const canSendMessages = useMemo(
235
251
  () => conversation.some((m) => m.side === "agent"),
236
- [conversation]
252
+ [conversation],
237
253
  );
238
254
 
239
255
  const handleSubmit = useCallback(
@@ -252,7 +268,7 @@ const AssistantChat = ({
252
268
  onSendMessage(text);
253
269
  }
254
270
  },
255
- [inputValue, onSendMessage, canSendMessages]
271
+ [inputValue, onSendMessage, canSendMessages],
256
272
  );
257
273
 
258
274
  const handleKeyDown = useCallback(
@@ -263,7 +279,7 @@ const AssistantChat = ({
263
279
  handleSubmit();
264
280
  }
265
281
  },
266
- [handleSubmit, canSendMessages]
282
+ [handleSubmit, canSendMessages],
267
283
  );
268
284
 
269
285
  const handleNewChat = useCallback(() => {
@@ -320,7 +336,7 @@ const AssistantChat = ({
320
336
  if (!entry) return;
321
337
  setShowScrollToBottom(!entry.isIntersecting);
322
338
  },
323
- { root, threshold: 0 }
339
+ { root, threshold: 0 },
324
340
  );
325
341
  io.observe(sentinel);
326
342
  return () => io.disconnect();
@@ -346,24 +362,56 @@ const AssistantChat = ({
346
362
 
347
363
  return (
348
364
  <div className={className}>
365
+ <div className="assistant-chat__header">
366
+ <span className="assistant-chat__header-title">
367
+ {title || <FormattedMessage id="assistant.newConversation" />}
368
+ </span>
369
+ <button
370
+ type="button"
371
+ className="assistant-chat__action assistant-chat__action--close"
372
+ onClick={onClose}
373
+ aria-label={formatMessage({ id: "assistant.actions.close" })}
374
+ >
375
+ <Icon name="close" />
376
+ </button>
377
+ </div>
349
378
  <div className="assistant-chat__actions">
350
379
  <button
351
380
  type="button"
352
- className="assistant-chat__action assistant-chat__action--new-chat"
381
+ className="assistant-chat__action"
353
382
  onClick={handleNewChat}
354
383
  aria-label={formatMessage({ id: "assistant.actions.newChat" })}
384
+ title={formatMessage({ id: "assistant.actions.newChat" })}
355
385
  >
356
386
  <Icon name="comment outline" />
357
387
  <FormattedMessage id="assistant.actions.newChat" />
358
388
  </button>
359
- <button
360
- type="button"
361
- className="assistant-chat__action assistant-chat__action--close"
362
- onClick={onClose}
363
- aria-label={formatMessage({ id: "assistant.actions.close" })}
364
- >
365
- <Icon name="close" />
366
- </button>
389
+ {onLoadLastConversation && (
390
+ <button
391
+ type="button"
392
+ className="assistant-chat__action"
393
+ onClick={onLoadLastConversation}
394
+ aria-label={formatMessage({ id: "assistant.actions.loadLast" })}
395
+ title={formatMessage({ id: "assistant.actions.loadLast" })}
396
+ >
397
+ <Icon name="history" />
398
+ <FormattedMessage id="assistant.actions.loadLast" />
399
+ </button>
400
+ )}
401
+ {onMoreConversations && (
402
+ <button
403
+ type="button"
404
+ className="assistant-chat__action"
405
+ onClick={onMoreConversations}
406
+ aria-label={formatMessage({
407
+ id: "assistant.actions.moreConversations",
408
+ })}
409
+ title={formatMessage({ id: "assistant.actions.moreConversations" })}
410
+ >
411
+ <Icon name="expand arrows alternate" />
412
+ <FormattedMessage id="assistant.actions.moreConversations" />
413
+ </button>
414
+ )}
367
415
  </div>
368
416
  <div className="assistant-chat__messages-wrapper">
369
417
  <div
@@ -386,7 +434,8 @@ const AssistantChat = ({
386
434
  const { msg } = item;
387
435
  const isP0 = msg.priority === 0;
388
436
  const isErrorP0 = msg.isError && isP0;
389
- const sourceLabel = isP0 && msg.source ? getSourceLabel(msg.source) : null;
437
+ const sourceLabel =
438
+ isP0 && msg.source ? getSourceLabel(msg.source) : null;
390
439
  return (
391
440
  <div
392
441
  key={item.key}
@@ -395,10 +444,14 @@ const AssistantChat = ({
395
444
  `assistant-chat__message--${msg.eventType}`,
396
445
  `assistant-chat__message--${msg.side}`,
397
446
  isErrorP0 && "assistant-chat__message--error",
398
- ].filter(Boolean).join(" ")}
447
+ ]
448
+ .filter(Boolean)
449
+ .join(" ")}
399
450
  >
400
451
  {sourceLabel && (
401
- <div className="assistant-chat__message-source">{sourceLabel}</div>
452
+ <div className="assistant-chat__message-source">
453
+ {sourceLabel}
454
+ </div>
402
455
  )}
403
456
  <div className="assistant-chat__message-content">
404
457
  {msg.content?.trim() ? (
@@ -431,7 +484,9 @@ const AssistantChat = ({
431
484
  type="button"
432
485
  className="assistant-chat__scroll-to-bottom"
433
486
  onClick={scrollToBottom}
434
- aria-label={formatMessage({ id: "assistant.actions.scrollToBottom" })}
487
+ aria-label={formatMessage({
488
+ id: "assistant.actions.scrollToBottom",
489
+ })}
435
490
  >
436
491
  <Icon name="chevron down" />
437
492
  </button>
@@ -481,17 +536,23 @@ AssistantChat.propTypes = {
481
536
  onNewChat: PropTypes.func,
482
537
  onClose: PropTypes.func.isRequired,
483
538
  onSendMessage: PropTypes.func,
539
+ onLoadLastConversation: PropTypes.func,
540
+ onMoreConversations: PropTypes.func,
484
541
  serverMessageHandlerRef: PropTypes.shape({ current: PropTypes.any }),
485
542
  fadeOut: PropTypes.bool,
486
543
  fadeIn: PropTypes.bool,
544
+ title: PropTypes.string,
487
545
  };
488
546
 
489
547
  AssistantChat.defaultProps = {
490
548
  onNewChat: undefined,
491
549
  onSendMessage: undefined,
550
+ onLoadLastConversation: undefined,
551
+ onMoreConversations: undefined,
492
552
  serverMessageHandlerRef: null,
493
553
  fadeOut: false,
494
554
  fadeIn: false,
555
+ title: null,
495
556
  };
496
557
 
497
558
  export default AssistantChat;