@stack-spot/ai-chat-widget 2.1.0 → 2.1.1-beta.1

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 (59) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/app-metadata.json +5 -5
  3. package/dist/chat-interceptors/send-message.d.ts.map +1 -1
  4. package/dist/chat-interceptors/send-message.js +125 -1
  5. package/dist/chat-interceptors/send-message.js.map +1 -1
  6. package/dist/state/ChatEntry.d.ts +1 -1
  7. package/dist/state/ChatEntry.d.ts.map +1 -1
  8. package/dist/state/ChatEntry.js +2 -1
  9. package/dist/state/ChatEntry.js.map +1 -1
  10. package/dist/state/ChatState.d.ts +4 -0
  11. package/dist/state/ChatState.d.ts.map +1 -1
  12. package/dist/state/ChatState.js.map +1 -1
  13. package/dist/utils/planning-tool.d.ts +17 -0
  14. package/dist/utils/planning-tool.d.ts.map +1 -0
  15. package/dist/utils/planning-tool.js +32 -0
  16. package/dist/utils/planning-tool.js.map +1 -0
  17. package/dist/utils/update-tool-step.d.ts +3 -0
  18. package/dist/utils/update-tool-step.d.ts.map +1 -0
  19. package/dist/utils/update-tool-step.js +23 -0
  20. package/dist/utils/update-tool-step.js.map +1 -0
  21. package/dist/views/Chat/ChatMessage.d.ts +1 -1
  22. package/dist/views/Chat/ChatMessage.d.ts.map +1 -1
  23. package/dist/views/Chat/ChatMessage.js +21 -6
  24. package/dist/views/Chat/ChatMessage.js.map +1 -1
  25. package/dist/views/Chat/StepsList.d.ts +12 -2
  26. package/dist/views/Chat/StepsList.d.ts.map +1 -1
  27. package/dist/views/Chat/StepsList.js +155 -18
  28. package/dist/views/Chat/StepsList.js.map +1 -1
  29. package/dist/views/Chat/styled.d.ts.map +1 -1
  30. package/dist/views/Chat/styled.js +17 -9
  31. package/dist/views/Chat/styled.js.map +1 -1
  32. package/dist/views/MessageInput/dictionary.d.ts +1 -1
  33. package/dist/views/Steps/FlowChart/NodeStep.js +1 -1
  34. package/dist/views/Steps/FlowChart/NodeStep.js.map +1 -1
  35. package/dist/views/Steps/FlowChart/layout.d.ts +1 -1
  36. package/dist/views/Steps/FlowChart/layout.d.ts.map +1 -1
  37. package/dist/views/Steps/FlowChart/layout.js +1 -0
  38. package/dist/views/Steps/FlowChart/layout.js.map +1 -1
  39. package/dist/views/Steps/FlowChart/types.d.ts +1 -1
  40. package/dist/views/Steps/FlowChart/types.d.ts.map +1 -1
  41. package/dist/views/Steps/StepModal.js +2 -2
  42. package/dist/views/Steps/StepModal.js.map +1 -1
  43. package/dist/views/Steps/dictionary.d.ts +1 -1
  44. package/dist/views/Steps/utils.d.ts +1 -1
  45. package/dist/views/Steps/utils.d.ts.map +1 -1
  46. package/package.json +3 -3
  47. package/src/app-metadata.json +5 -5
  48. package/src/chat-interceptors/send-message.ts +137 -2
  49. package/src/state/ChatEntry.ts +2 -1
  50. package/src/state/ChatState.ts +4 -0
  51. package/src/utils/planning-tool.ts +41 -0
  52. package/src/utils/update-tool-step.tsx +27 -0
  53. package/src/views/Chat/ChatMessage.tsx +25 -5
  54. package/src/views/Chat/StepsList.tsx +337 -44
  55. package/src/views/Chat/styled.ts +17 -9
  56. package/src/views/Steps/FlowChart/NodeStep.tsx +1 -1
  57. package/src/views/Steps/FlowChart/layout.ts +1 -0
  58. package/src/views/Steps/FlowChart/types.ts +1 -1
  59. package/src/views/Steps/StepModal.tsx +2 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stack-spot/ai-chat-widget",
3
- "version": "2.1.0",
3
+ "version": "2.1.1-beta.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -11,12 +11,12 @@
11
11
  },
12
12
  "peerDependencies": {
13
13
  "@stack-spot/citric-react": "^0.36.0",
14
- "@stack-spot/citric-icons": "^0.2.2",
14
+ "@stack-spot/citric-icons": "^0.2.3",
15
15
  "@stack-spot/portal-theme": "^1.2.1",
16
16
  "@citric/core": "^6.4.0",
17
17
  "@stack-spot/portal-components": "^2.26.0",
18
18
  "@citric/icons": "^5.13.0",
19
- "@stack-spot/portal-network": "0.181.0",
19
+ "@stack-spot/portal-network": "0.186.0-beta.1",
20
20
  "@citric/ui": "^6.10.2",
21
21
  "@stack-spot/portal-translate": "^2.1.0",
22
22
  "lodash": "^4.17.0",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stack-spot/ai-chat-widget",
3
- "version": "2.1.0",
4
- "date": "Wed Oct 01 2025 16:50:55 GMT+0000 (Coordinated Universal Time)",
3
+ "version": "2.1.1-beta.1",
4
+ "date": "Thu Oct 16 2025 13:16:47 GMT+0000 (Coordinated Universal Time)",
5
5
  "dependencies": [
6
6
  {
7
7
  "name": "@stack-spot/app-metadata",
@@ -109,11 +109,11 @@
109
109
  },
110
110
  {
111
111
  "name": "@stack-spot/citric-icons",
112
- "version": "0.2.2"
112
+ "version": "0.2.3"
113
113
  },
114
114
  {
115
115
  "name": "@stack-spot/citric-react",
116
- "version": "0.36.0(@stack-spot/citric-icons@0.2.2)(@stack-spot/portal-theme@1.2.1(@citric/core@6.4.0(lodash@4.17.21)(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0)))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0)))(@stack-spot/portal-translate@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(lodash@4.17.21)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)"
116
+ "version": "0.36.0(@stack-spot/citric-icons@0.2.3)(@stack-spot/portal-theme@1.2.1(@citric/core@6.4.0(lodash@4.17.21)(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0)))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0)))(@stack-spot/portal-translate@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(lodash@4.17.21)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)"
117
117
  },
118
118
  {
119
119
  "name": "@stack-spot/portal-components",
@@ -121,7 +121,7 @@
121
121
  },
122
122
  {
123
123
  "name": "@stack-spot/portal-network",
124
- "version": "0.181.0(@stack-spot/auth@6.1.0)(@stack-spot/opa@2.5.0(@stack-spot/auth@6.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@stack-spot/portal-translate@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@tanstack/react-query@5.59.16(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)"
124
+ "version": "0.186.0-beta.1(@stack-spot/auth@6.1.0)(@stack-spot/opa@2.5.0(@stack-spot/auth@6.1.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@stack-spot/portal-translate@2.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@tanstack/react-query@5.59.16(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)"
125
125
  },
126
126
  {
127
127
  "name": "@stack-spot/portal-theme",
@@ -1,11 +1,13 @@
1
- import { aiClient, ChatResponseWithSteps, StackspotAPIError, StreamCanceledError } from '@stack-spot/portal-network'
1
+ import { AgentInfo, aiClient, ChatResponseWithSteps, ChatStep, StackspotAPIError, StreamCanceledError } from '@stack-spot/portal-network'
2
2
  import { ChatResponse3 } from '@stack-spot/portal-network/api/ai'
3
+ import { findLast } from 'lodash'
3
4
  import { ChatEntry, KnowledgeSource, TextChatEntry } from '../state/ChatEntry'
4
5
  import { ChatState } from '../state/ChatState'
5
6
  import { LabeledWithImage } from '../state/types'
6
7
  import { buildConversationContext } from '../utils/chat'
7
8
  import { treatHTMLInErrorMessage } from '../utils/error'
8
9
  import { genericSourcesToKnowledgeSources } from '../utils/knowledge-source'
10
+ import { planningToolDictionaryHelper } from '../utils/planning-tool'
9
11
 
10
12
  /**
11
13
  * Transforms a chat response from the backend into a chat entry that can be added to the chat.
@@ -22,7 +24,7 @@ function createEntryValueFromChatResponse(
22
24
  agent: LabeledWithImage | undefined,
23
25
  includeDate = false,
24
26
  ): TextChatEntry {
25
- return {
27
+ const entry= {
26
28
  agentType: 'bot',
27
29
  type: 'md',
28
30
  content: response.answer ?? '',
@@ -33,6 +35,7 @@ function createEntryValueFromChatResponse(
33
35
  steps: response.steps,
34
36
  tools: response.tools,
35
37
  }
38
+ return entry as TextChatEntry
36
39
  }
37
40
 
38
41
  function buildPrompt(content: string, data?: any) {
@@ -61,6 +64,18 @@ export async function sendMessageInterceptor(entry: ChatEntry, chat: ChatState,
61
64
  chat.set('label', content || entry.getValue().upload?.[0]?.name || 'Chat')
62
65
  chat.untitled = false
63
66
  }
67
+
68
+ //Verify if the last planning in the messages has status awaiting_approval
69
+ const messages = chat.getMessages()
70
+ const lastPlanningAwaiting = findLast(messages, item => {
71
+ const steps = item.getValue().steps
72
+ if (steps) {
73
+ const hasPlanning = steps.find((step) => step.type === 'planning')
74
+ return hasPlanning ? hasPlanning.status === 'awaiting_approval' : false
75
+ }
76
+ return false
77
+ })
78
+
64
79
  const stream = aiClient.sendChatMessage({ context, user_prompt: buildPrompt(content, data) })
65
80
  signal.addEventListener('abort', () => stream.cancel())
66
81
  const botEntry = ChatEntry.createStreamedBotEntry()
@@ -69,15 +84,135 @@ export async function sendMessageInterceptor(entry: ChatEntry, chat: ChatState,
69
84
  chat.pushMessage(botEntry)
70
85
  }
71
86
  let knowledgeSources: KnowledgeSource[] | undefined
87
+
88
+ const updatePlanningMessage = () => {
89
+ if (lastPlanningAwaiting) {
90
+ const originalItem = messages.find((message) => message.id === lastPlanningAwaiting.id)
91
+ const originalItemValue = originalItem?.getValue()
92
+ originalItemValue?.steps?.map((step) => {
93
+ if (step.type === 'planning' && step.status === 'awaiting_approval') {
94
+ step.status = 'success'
95
+ }
96
+ })
97
+ originalItem?.setValue({ ...originalItemValue as TextChatEntry })
98
+ }
99
+ }
100
+
101
+ const updateStepMessage = (step: ChatStep) => {
102
+ if (lastPlanningAwaiting) {
103
+ const originalItem = messages.find((message) => message.id === lastPlanningAwaiting.id)
104
+ const originalItemValue = originalItem?.getValue()
105
+ originalItemValue?.steps?.filter((messageStep) => {
106
+ if (messageStep.id === step.id) {
107
+ messageStep = { ...step }
108
+ }
109
+ })
110
+ originalItem?.setValue({ ...originalItemValue as TextChatEntry })
111
+ }
112
+ }
113
+
114
+ const updateToolStatus = (agentInfo: AgentInfo) => {
115
+ const executionId = agentInfo.id
116
+ if (executionId) {
117
+ //Update message with type step which contains the planning steps
118
+ const messageId = planningToolDictionaryHelper.getMessageIdPlanningStepFromToolExecutionId(executionId)
119
+ const originalItem = messages.find((message) => `${message.id}` === messageId)
120
+ const originalItemValue = originalItem?.getValue()
121
+ let update = false
122
+ const status = agentInfo.action === 'start' ? 'running' : 'success'
123
+ const step = originalItemValue?.steps?.find(step => step.type === 'step' && step.attempts?.[0].tools?.[0].executionId === executionId)
124
+ if (step && step.status !== status) {
125
+ step.status = status
126
+ update = true
127
+ }
128
+ if (update) {
129
+ originalItem?.setValue({ ...originalItemValue as TextChatEntry })
130
+ }
131
+
132
+ //Updates message with type tool which contains the actually tool steps
133
+ //We only want to show tools banner when they are awaiting_approval, by removing the step
134
+ // we avoid the entire bot message to be visible
135
+ const toolMessageId = planningToolDictionaryHelper.getMessageIdToolStepFromToolExecutionId(executionId)
136
+ const toolOriginalItem = messages.find((message) => `${message.id}` === toolMessageId)
137
+ const toolOriginalItemValue = toolOriginalItem?.getValue()
138
+ const toolStep = toolOriginalItemValue?.steps?.find(step =>
139
+ step.type === 'tool' && step.attempts?.[0].tools?.[0].executionId === executionId)
140
+ update = false
141
+ if (toolOriginalItemValue && toolStep && toolStep.status !== status) {
142
+ toolOriginalItemValue.steps = undefined
143
+ update = true
144
+ }
145
+ if (update) {
146
+ toolOriginalItem?.setValue({ ...toolOriginalItemValue as TextChatEntry })
147
+ }
148
+ }
149
+ }
150
+
72
151
  stream.onChange(value => {
152
+ if (value.agent_info?.type === 'planning') {
153
+ if (value.agent_info.action === 'start') {
154
+ chat.set('isPlaning', true)
155
+ } else {
156
+ chat.set('isPlaning', false)
157
+ }
158
+ }
159
+
73
160
  if (value.sources?.length !== knowledgeSources?.length && chat.get('features').showSourcesInResponse) {
74
161
  knowledgeSources = genericSourcesToKnowledgeSources(value.sources)
75
162
  }
163
+
164
+ if (lastPlanningAwaiting && value.steps) {
165
+ value.steps?.map(step => {
166
+ if (step.type === 'planning') {
167
+ updatePlanningMessage()
168
+ } else if (step.type === 'step') {
169
+ updateStepMessage(step)
170
+ }
171
+ })
172
+ }
173
+
174
+ if (value.agent_info?.type === 'tool' && value.agent_info?.action !== 'awaiting_approval') {
175
+ updateToolStatus(value.agent_info)
176
+ }
177
+
178
+ if (value.steps) {
179
+ const tool = findLast(value.steps, (item) => item.type === 'tool')
180
+
181
+ if (tool && tool.status === 'running') {
182
+ const messageId = planningToolDictionaryHelper.getMessageIdPlanningStepFromToolExecutionId(tool.id)
183
+ const originalItem = messages.find((message) => `${message.id}` === messageId)
184
+ const originalItemValue = originalItem?.getValue()
185
+ let update = false
186
+ const step = originalItemValue?.steps?.find(step => step.type === 'step')
187
+ if (step && step.attempts?.[0].tools?.[0].executionId === tool.id) {
188
+ step.attempts?.map((attempt, i) => {
189
+ const newAttempt = tool.attempts?.[i]
190
+ if (!newAttempt) return
191
+ attempt.tools?.map((origTool, j) => {
192
+ const newTool = newAttempt.tools?.[j]
193
+ if (!newTool || origTool.executionId !== newTool.executionId) return origTool
194
+ update = true
195
+ return { ...origTool, ...newTool }
196
+ })
197
+ })
198
+ }
199
+ if (update) {
200
+ originalItem?.setValue({ ...originalItemValue as TextChatEntry })
201
+ }
202
+ }
203
+ }
204
+
76
205
  botEntry.setValue(createEntryValueFromChatResponse(value, knowledgeSources, chat.get('agent')))
77
206
  })
207
+
78
208
  let finalValue: Partial<ChatResponse3> | undefined
79
209
  try {
80
210
  finalValue = await stream.getValue()
211
+ if (lastPlanningAwaiting) {
212
+ const value = lastPlanningAwaiting.getValue()
213
+ value.content = finalValue.answer || value.content
214
+ lastPlanningAwaiting.setValue(value)
215
+ }
81
216
  // if the streaming feature is not enabled, we only add the chat entry once the streaming has finished
82
217
  if (!chat.get('features').streaming) {
83
218
  chat.pushMessage(botEntry)
@@ -205,13 +205,14 @@ export class ChatEntry {
205
205
  * @param hiddenContent the message's content.
206
206
  * @returns a new ChatEntry.
207
207
  */
208
- static createUserEntry(content: string, isMd = false, fieldName?: string, hiddenContent?: string[]) {
208
+ static createUserEntry(content: string, isMd = false, fieldName?: string, hiddenContent?: string[], data?: any) {
209
209
  return new ChatEntry({
210
210
  agentType: 'user',
211
211
  type: isMd ? 'md' : 'text',
212
212
  content,
213
213
  name: fieldName,
214
214
  hiddenContent,
215
+ data,
215
216
  updated: new Date().toISOString(),
216
217
  })
217
218
  }
@@ -33,6 +33,10 @@ export interface ChatPropertiesWithOptionalFeatures {
33
33
  * Whether or not the chat is in a loading state.
34
34
  */
35
35
  isLoading?: boolean,
36
+ /**
37
+ * Whether or not the chat is planning.
38
+ */
39
+ isPlaning?: boolean,
36
40
  /**
37
41
  * The value of the next message. This is the value of the text typed in the textarea below the chat.
38
42
  */
@@ -0,0 +1,41 @@
1
+ class PlanningToolDictionaryHelper {
2
+ static instance: PlanningToolDictionaryHelper | undefined
3
+ private toolExecutionIdPlanningStep: Record<string, string> = {}
4
+ private toolExecutionIdToolStep: Record<string, string> = {}
5
+ private stepId: Record<string, number> = {}
6
+
7
+ private constructor() {
8
+ PlanningToolDictionaryHelper.instance = this
9
+ }
10
+
11
+ static create() {
12
+ return PlanningToolDictionaryHelper.instance ?? new PlanningToolDictionaryHelper()
13
+ }
14
+
15
+ setMessageIdPlanningStepToolExecutionId(messageId: string, toolExecutionId: string){
16
+ this.toolExecutionIdPlanningStep[toolExecutionId] = messageId
17
+ }
18
+
19
+ setMessageIdToolStepToolExecutionId(messageId: string, toolExecutionId: string){
20
+ this.toolExecutionIdToolStep[toolExecutionId] = messageId
21
+ }
22
+
23
+ setMessageIdForStepId(messageId: number, stepId: string){
24
+ this.stepId[stepId] = messageId
25
+ }
26
+
27
+ getMessageIdFromStepId(stepId: string){
28
+ return this.stepId[stepId]
29
+ }
30
+
31
+ getMessageIdPlanningStepFromToolExecutionId(toolExecutionId: string){
32
+ return this.toolExecutionIdPlanningStep[toolExecutionId]
33
+ }
34
+
35
+ getMessageIdToolStepFromToolExecutionId(toolExecutionId: string){
36
+ return this.toolExecutionIdToolStep[toolExecutionId]
37
+ }
38
+ }
39
+
40
+ export const planningToolDictionaryHelper = PlanningToolDictionaryHelper.create()
41
+
@@ -0,0 +1,27 @@
1
+ import { ChatEntry, TextChatEntry } from '../state/ChatEntry'
2
+ import { planningToolDictionaryHelper } from './planning-tool'
3
+
4
+ export const updateToolStep = (messages: ChatEntry[], executionId: string,
5
+ newStatus: 'pending' | 'running' | 'success' | 'error' | 'awaiting_approval') => {
6
+
7
+ // if last message is a user message, no update in tool status is needed
8
+ if (messages[messages.length-1].getValue().agentType === 'user') return
9
+
10
+ const messageId = planningToolDictionaryHelper.getMessageIdPlanningStepFromToolExecutionId(executionId)
11
+ const message = messages.find((message) => `${message.id}` === messageId)
12
+ let update = false
13
+ const messageValue = message?.getValue()
14
+ messageValue?.steps?.map((step) => {
15
+ if (step.type === 'step') {
16
+ const tool = step.attempts?.[0].tools?.[0]
17
+ if (tool?.executionId === executionId && step.status !== newStatus) {
18
+ step.status = newStatus
19
+ update = true
20
+ }
21
+ }
22
+ })
23
+
24
+ if (update) {
25
+ message?.setValue({ ...messageValue as TextChatEntry })
26
+ }
27
+ }
@@ -9,7 +9,7 @@ import { PhoneInput } from 'react-international-phone'
9
9
  import 'react-international-phone/style.css'
10
10
  import { FileDescription } from '../../components/FileDescription'
11
11
  import { Markdown } from '../../components/Markdown'
12
- import { useChatEntry, useCurrentChat, useWidget } from '../../context/hooks'
12
+ import { useChatEntry, useChatMessages, useCurrentChat, useCurrentChatState, useWidget } from '../../context/hooks'
13
13
  import { useMidnightUpdateView } from '../../hooks/midnight-update-view'
14
14
  import { ChatEntry, SerializableAction, TextChatEntry } from '../../state/ChatEntry'
15
15
  import { useDateFormatter } from '../../utils/date'
@@ -17,7 +17,7 @@ import { toolById } from '../../utils/tools'
17
17
  import { AgentInfo } from './AgentInfo'
18
18
  import { useChatScrollToBottomEffect } from './chat-scroll'
19
19
  import { onCopyAll, onCopyCode, onLikeOrDislike } from './events'
20
- import { StepsList } from './StepsList'
20
+ import { StepsList, StepsPlaceholder, ViewToolsDetails } from './StepsList'
21
21
 
22
22
  export interface CustomRenderResult {
23
23
  /**
@@ -218,7 +218,21 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
218
218
  { searchAgentsRequest: { ids: entry.tools || [''] } }, { enabled: !!entry.tools })
219
219
  const [copied, setCopied] = useState(false)
220
220
  const [showUserButtonCopy, setShowUserButtonCopy] = useState(false)
221
-
221
+ const isPlanning = useCurrentChatState('isPlaning') ?? false
222
+
223
+ // when we have a steps but we are not showing any content of the step
224
+ // (because it is a tool and the user has already answered the question)
225
+ // we do not want to show an avatar with empty content, so we hide the entire message
226
+ const toolsStep = entry.steps?.find(s => s.type === 'tool')
227
+ const messages = useChatMessages(chat.id)
228
+ const userHasAlreadyAnswered = useMemo(() => {
229
+ const messageIndex = messages.findIndex((messageItem) => messageItem.id === message.id)
230
+ if (messages.length-1 === messageIndex) return false
231
+ const nextMessage = messages[messageIndex+1].getValue()
232
+ return nextMessage.agentType === 'user'
233
+ }, [messages, messages.length])
234
+ const isMessageHidden = toolsStep && userHasAlreadyAnswered
235
+
222
236
  useChatScrollToBottomEffect(ref, [entry])
223
237
  useMidnightUpdateView()
224
238
 
@@ -343,7 +357,8 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
343
357
  widget.set('panel', 'resources')
344
358
  }
345
359
 
346
- return (entry.content || entry.error || !!entry.steps?.length || entry.upload?.length) && (
360
+ return (entry.content || entry.error || !!entry.steps?.length ||
361
+ entry.upload?.length) && (!isMessageHidden || !toolsStep || isPlanning) && (
347
362
  <li key={entry.messageId} className={entry.agentType} ref={ref}>
348
363
  <div className="chat-message-container"
349
364
  onMouseEnter={entry.agentType === 'user' ? () => setShowUserButtonCopy(true) : undefined}
@@ -356,11 +371,15 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
356
371
  {!!entry.badges?.length && <div className="badges">
357
372
  {entry.badges.map((b, index) => <Badge key={index} colorPalette={b.color ?? 'cyan'} appearance="square">{b.label}</Badge>)}
358
373
  </div>}
374
+
375
+ {!!entry.steps?.length && <StepsList steps={entry.steps} chatId={chat.id} messageId={message.id}
376
+ userHasAlreadyAnswered={userHasAlreadyAnswered} />}
377
+
359
378
  {renderContent()}
360
379
 
361
- {!!entry.steps?.length && <StepsList steps={entry.steps} chatId={chat.id} messageId={message.id} />}
362
380
  </div>
363
381
  )}
382
+ {isPlanning && entry.agentType === 'bot' && isLast && <StepsPlaceholder /> }
364
383
 
365
384
  {entry.error && <Alert type="error">{entry.error}</Alert>}
366
385
  </div>
@@ -416,6 +435,7 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
416
435
  </ImageBox>
417
436
  )})}
418
437
  </Button>
438
+ <ViewToolsDetails chatId={chat.id} />
419
439
  </div>}
420
440
 
421
441
  {shouldShowFooter && <div className="message-footer">