@stack-spot/ai-chat-widget 3.0.0-beta.1 → 3.0.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 (82) hide show
  1. package/CHANGELOG.md +77 -19
  2. package/dist/app-metadata.json +6 -6
  3. package/dist/chat-interceptors/quick-commands.d.ts.map +1 -1
  4. package/dist/chat-interceptors/quick-commands.js +10 -3
  5. package/dist/chat-interceptors/quick-commands.js.map +1 -1
  6. package/dist/chat-interceptors/send-message.d.ts.map +1 -1
  7. package/dist/chat-interceptors/send-message.js +76 -15
  8. package/dist/chat-interceptors/send-message.js.map +1 -1
  9. package/dist/layout.css +6 -0
  10. package/dist/state/ChatEntry.d.ts +1 -1
  11. package/dist/state/ChatEntry.d.ts.map +1 -1
  12. package/dist/state/ChatEntry.js +2 -1
  13. package/dist/state/ChatEntry.js.map +1 -1
  14. package/dist/state/ChatState.d.ts +4 -0
  15. package/dist/state/ChatState.d.ts.map +1 -1
  16. package/dist/state/ChatState.js.map +1 -1
  17. package/dist/utils/chat.d.ts.map +1 -1
  18. package/dist/utils/chat.js +1 -0
  19. package/dist/utils/chat.js.map +1 -1
  20. package/dist/utils/knowledge-source.d.ts +2 -2
  21. package/dist/utils/planning-tool.d.ts +3 -0
  22. package/dist/utils/planning-tool.d.ts.map +1 -1
  23. package/dist/utils/planning-tool.js +7 -0
  24. package/dist/utils/planning-tool.js.map +1 -1
  25. package/dist/views/Agents/styled.d.ts.map +1 -1
  26. package/dist/views/Agents/styled.js +1 -2
  27. package/dist/views/Agents/styled.js.map +1 -1
  28. package/dist/views/Chat/ChatMessage.d.ts +1 -1
  29. package/dist/views/Chat/ChatMessage.d.ts.map +1 -1
  30. package/dist/views/Chat/ChatMessage.js +23 -6
  31. package/dist/views/Chat/ChatMessage.js.map +1 -1
  32. package/dist/views/Chat/StepsList.d.ts +8 -3
  33. package/dist/views/Chat/StepsList.d.ts.map +1 -1
  34. package/dist/views/Chat/StepsList.js +67 -34
  35. package/dist/views/Chat/StepsList.js.map +1 -1
  36. package/dist/views/Chat/styled.d.ts.map +1 -1
  37. package/dist/views/Chat/styled.js +8 -12
  38. package/dist/views/Chat/styled.js.map +1 -1
  39. package/dist/views/MessageInput/ButtonBar.d.ts.map +1 -1
  40. package/dist/views/MessageInput/ButtonBar.js +2 -1
  41. package/dist/views/MessageInput/ButtonBar.js.map +1 -1
  42. package/dist/views/MessageInput/ModelSwitcher/index.d.ts +2 -0
  43. package/dist/views/MessageInput/ModelSwitcher/index.d.ts.map +1 -0
  44. package/dist/views/MessageInput/ModelSwitcher/index.js +25 -0
  45. package/dist/views/MessageInput/ModelSwitcher/index.js.map +1 -0
  46. package/dist/views/MessageInput/ModelSwitcher/utils.d.ts +30 -0
  47. package/dist/views/MessageInput/ModelSwitcher/utils.d.ts.map +1 -0
  48. package/dist/views/MessageInput/ModelSwitcher/utils.js +91 -0
  49. package/dist/views/MessageInput/ModelSwitcher/utils.js.map +1 -0
  50. package/dist/views/MessageInput/SelectContent.js +1 -1
  51. package/dist/views/MessageInput/SelectContent.js.map +1 -1
  52. package/dist/views/MessageInput/dictionary.d.ts +1 -1
  53. package/dist/views/MessageInput/dictionary.d.ts.map +1 -1
  54. package/dist/views/MessageInput/dictionary.js +6 -0
  55. package/dist/views/MessageInput/dictionary.js.map +1 -1
  56. package/dist/views/MessageInput/styled.d.ts +12 -0
  57. package/dist/views/MessageInput/styled.d.ts.map +1 -1
  58. package/dist/views/MessageInput/styled.js +35 -0
  59. package/dist/views/MessageInput/styled.js.map +1 -1
  60. package/dist/views/Resources.js +12 -5
  61. package/dist/views/Resources.js.map +1 -1
  62. package/package.json +4 -4
  63. package/src/app-metadata.json +6 -6
  64. package/src/chat-interceptors/quick-commands.ts +18 -7
  65. package/src/chat-interceptors/send-message.ts +82 -18
  66. package/src/layout.css +6 -0
  67. package/src/state/ChatEntry.ts +2 -1
  68. package/src/state/ChatState.ts +4 -0
  69. package/src/utils/chat.ts +1 -0
  70. package/src/utils/knowledge-source.ts +2 -2
  71. package/src/utils/planning-tool.ts +9 -0
  72. package/src/views/Agents/styled.ts +1 -2
  73. package/src/views/Chat/ChatMessage.tsx +63 -57
  74. package/src/views/Chat/StepsList.tsx +115 -72
  75. package/src/views/Chat/styled.ts +8 -12
  76. package/src/views/MessageInput/ButtonBar.tsx +2 -0
  77. package/src/views/MessageInput/ModelSwitcher/index.tsx +68 -0
  78. package/src/views/MessageInput/ModelSwitcher/utils.tsx +143 -0
  79. package/src/views/MessageInput/SelectContent.tsx +1 -1
  80. package/src/views/MessageInput/dictionary.ts +6 -0
  81. package/src/views/MessageInput/styled.ts +37 -0
  82. package/src/views/Resources.tsx +18 -10
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stack-spot/ai-chat-widget",
3
- "version": "3.0.0-beta.1",
4
- "date": "Wed Oct 01 2025 15:13:49 GMT+0000 (Coordinated Universal Time)",
3
+ "version": "3.0.1-beta.1",
4
+ "date": "Fri Nov 07 2025 20:13:12 GMT+0000 (Coordinated Universal Time)",
5
5
  "dependencies": [
6
6
  {
7
7
  "name": "@stack-spot/app-metadata",
@@ -109,19 +109,19 @@
109
109
  },
110
110
  {
111
111
  "name": "@stack-spot/citric-icons",
112
- "version": "0.2.3"
112
+ "version": "0.2.5"
113
113
  },
114
114
  {
115
115
  "name": "@stack-spot/citric-react",
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)"
116
+ "version": "0.36.0(@stack-spot/citric-icons@0.2.5)(@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",
120
- "version": "2.26.0(@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)))(@citric/icons@5.13.0(react@18.2.0))(@citric/ui@6.10.2(@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)))(@citric/icons@5.13.0(react@18.2.0))(lodash@4.17.21)(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-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))(@types/react@18.3.11)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)"
120
+ "version": "2.27.3(@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)))(@citric/icons@5.13.0(react@18.2.0))(@citric/ui@6.10.2(@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)))(@citric/icons@5.13.0(react@18.2.0))(lodash@4.17.21)(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-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))(@types/react@18.3.11)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)"
121
121
  },
122
122
  {
123
123
  "name": "@stack-spot/portal-network",
124
- "version": "0.179.1-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)"
124
+ "version": "0.195.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",
@@ -67,6 +67,10 @@ export function createQuickCommandInterceptor(widget: WidgetState, getEditor: ()
67
67
  chat.set('isLoading', true)
68
68
  CustomInputs.deleteCustomInputsFromChat(chat)
69
69
  ctx.customInputs = { ...ctx.customInputs, ...inputsValues }
70
+ ctx.context.upload_ids = chat.getMessages()
71
+ .flatMap(message =>
72
+ (message.getValue().upload || []).map(upload => upload.id),
73
+ )
70
74
  }
71
75
 
72
76
  /**
@@ -101,9 +105,9 @@ export function createQuickCommandInterceptor(widget: WidgetState, getEditor: ()
101
105
  return runStepsRecursively(stepIndex, progress, ctx, iteration)
102
106
  }
103
107
  const nextStepIndex = steps?.findIndex((step) => step.slug === next_step_slug)
104
-
108
+
105
109
  if (isNil(nextStepIndex) || nextStepIndex === -1) return
106
-
110
+
107
111
  return runStepsRecursively(nextStepIndex, progress, ctx, iteration)
108
112
  }
109
113
  catch (error: any) {
@@ -173,8 +177,8 @@ export function createQuickCommandInterceptor(widget: WidgetState, getEditor: ()
173
177
  }
174
178
 
175
179
  const stream = aiClient.streamLlmStepOfQuickCommand(
176
- slug,
177
- step.slug,
180
+ slug,
181
+ step.slug,
178
182
  {
179
183
  input_data: code,
180
184
  context: stepContext,
@@ -241,9 +245,16 @@ export function createQuickCommandInterceptor(widget: WidgetState, getEditor: ()
241
245
  await (currentStep.type === 'FETCH' ? runFetchStep(ctx, currentIndex) : runLLMStep(ctx, currentIndex))
242
246
 
243
247
  let nextIndex = currentIndex + 1
244
- if (currentStep.next_step_slug) {
245
- nextIndex = currentStep.next_step_slug === 'end' ?
246
- qc.steps.length : qc.steps?.findIndex((step) => step.slug === currentStep.next_step_slug)
248
+
249
+ let nextStepSlug = currentStep.next_step_slug
250
+ const stepResult = ctx.resultMap[currentStep.slug]
251
+ if (stepResult && typeof stepResult !== 'string' && 'answer_status' in stepResult && !!stepResult.answer_status?.next_step_slug) {
252
+ nextStepSlug = stepResult.answer_status.next_step_slug
253
+ }
254
+
255
+ if (nextStepSlug) {
256
+ nextIndex =nextStepSlug === 'end' ?
257
+ qc.steps.length : qc.steps?.findIndex((step) => step.slug === nextStepSlug)
247
258
  }
248
259
  await runStepsRecursively(nextIndex, progress, ctx, iteration)
249
260
  }
@@ -1,5 +1,6 @@
1
- import { AgentInfo, 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'
@@ -23,7 +24,7 @@ function createEntryValueFromChatResponse(
23
24
  agent: LabeledWithImage | undefined,
24
25
  includeDate = false,
25
26
  ): TextChatEntry {
26
- return {
27
+ const entry = {
27
28
  agentType: 'bot',
28
29
  type: 'md',
29
30
  content: response.answer ?? '',
@@ -34,6 +35,7 @@ function createEntryValueFromChatResponse(
34
35
  steps: response.steps,
35
36
  tools: response.tools,
36
37
  }
38
+ return entry as TextChatEntry
37
39
  }
38
40
 
39
41
  function buildPrompt(content: string, data?: any) {
@@ -63,9 +65,10 @@ export async function sendMessageInterceptor(entry: ChatEntry, chat: ChatState,
63
65
  chat.untitled = false
64
66
  }
65
67
 
66
- //Verify if the last planning in the messages has status awaiting_approval
67
68
  const messages = chat.getMessages()
68
- const lastPlanningAwaiting = messages.slice().reverse().find(item => {
69
+
70
+ //Verify if the last planning in the messages has status awaiting_approval
71
+ const getLastPlanningAwaiting = () => findLast(messages, item => {
69
72
  const steps = item.getValue().steps
70
73
  if (steps) {
71
74
  const hasPlanning = steps.find((step) => step.type === 'planning')
@@ -84,6 +87,8 @@ export async function sendMessageInterceptor(entry: ChatEntry, chat: ChatState,
84
87
  let knowledgeSources: KnowledgeSource[] | undefined
85
88
 
86
89
  const updatePlanningMessage = () => {
90
+ const lastPlanningAwaiting = getLastPlanningAwaiting()
91
+
87
92
  if (lastPlanningAwaiting) {
88
93
  const originalItem = messages.find((message) => message.id === lastPlanningAwaiting.id)
89
94
  const originalItemValue = originalItem?.getValue()
@@ -96,6 +101,21 @@ export async function sendMessageInterceptor(entry: ChatEntry, chat: ChatState,
96
101
  }
97
102
  }
98
103
 
104
+ const updateStepMessage = (step: ChatStep) => {
105
+ const lastPlanningAwaiting = getLastPlanningAwaiting()
106
+
107
+ if (lastPlanningAwaiting) {
108
+ const originalItem = messages.find((message) => message.id === lastPlanningAwaiting.id)
109
+ const originalItemValue = originalItem?.getValue()
110
+ originalItemValue?.steps?.filter((messageStep) => {
111
+ if (messageStep.id === step.id) {
112
+ messageStep = { ...step }
113
+ }
114
+ })
115
+ originalItem?.setValue({ ...originalItemValue as TextChatEntry })
116
+ }
117
+ }
118
+
99
119
  const updateToolStatus = (agentInfo: AgentInfo) => {
100
120
  const executionId = agentInfo.id
101
121
  if (executionId) {
@@ -104,8 +124,9 @@ export async function sendMessageInterceptor(entry: ChatEntry, chat: ChatState,
104
124
  const originalItem = messages.find((message) => `${message.id}` === messageId)
105
125
  const originalItemValue = originalItem?.getValue()
106
126
  let update = false
107
- const status = agentInfo.action === 'start' ? 'pending' : 'success'
108
- const step = originalItemValue?.steps?.find(step => step.type === 'step' && step.attempts?.[0].tools?.[0].executionId === executionId)
127
+ const status = agentInfo.action === 'start' ? 'running' : 'success'
128
+ const step = originalItemValue?.steps
129
+ ?.find(step => step.type === 'step' && step.attempts?.[0]?.tools?.[0]?.executionId === executionId)
109
130
  if (step && step.status !== status) {
110
131
  step.status = status
111
132
  update = true
@@ -115,14 +136,16 @@ export async function sendMessageInterceptor(entry: ChatEntry, chat: ChatState,
115
136
  }
116
137
 
117
138
  //Updates message with type tool which contains the actually tool steps
139
+ //We only want to show tools banner when they are awaiting_approval, by removing the step
140
+ // we avoid the entire bot message to be visible
118
141
  const toolMessageId = planningToolDictionaryHelper.getMessageIdToolStepFromToolExecutionId(executionId)
119
142
  const toolOriginalItem = messages.find((message) => `${message.id}` === toolMessageId)
120
143
  const toolOriginalItemValue = toolOriginalItem?.getValue()
121
- const toolStep = toolOriginalItemValue?.steps?.find(step =>
122
- step.type === 'tool' && step.attempts?.[0].tools?.[0].executionId === executionId)
144
+ const toolStep = toolOriginalItemValue?.steps?.find(step =>
145
+ step.type === 'tool' && step.attempts?.[0]?.tools?.[0]?.executionId === executionId)
123
146
  update = false
124
- if (toolStep && toolStep.status !== status) {
125
- toolStep.status = status
147
+ if (toolOriginalItemValue && toolStep && toolStep.status !== status) {
148
+ toolOriginalItemValue.steps = undefined
126
149
  update = true
127
150
  }
128
151
  if (update) {
@@ -140,34 +163,75 @@ export async function sendMessageInterceptor(entry: ChatEntry, chat: ChatState,
140
163
  }
141
164
  }
142
165
 
166
+ if (value.agent_info?.type === 'chat' && value.agent_info?.action === 'end') {
167
+ //When an error happens, the step can still be running, so we enforce the error
168
+ const stepRunning = findLast(value.steps, (item) => item.status === 'running')
169
+ if (stepRunning?.status) {
170
+ stepRunning.status = 'error'
171
+ }
172
+ }
173
+
143
174
  if (value.sources?.length !== knowledgeSources?.length && chat.get('features').showSourcesInResponse) {
144
175
  knowledgeSources = genericSourcesToKnowledgeSources(value.sources)
145
176
  }
146
177
 
147
- // When there is a planning with status awaiting_approval and we receive a new one
148
- // we do not not want to add it again.
178
+ const lastPlanningAwaiting = getLastPlanningAwaiting()
149
179
  if (lastPlanningAwaiting && value.steps) {
150
- const hasPlanningAwaiting = value.steps.find((item) => item.type === 'planning' && item.status === 'awaiting_approval')
151
- value.steps = value.steps?.filter(step => {
180
+ value.steps?.map(step => {
152
181
  if (step.type === 'planning') {
153
182
  updatePlanningMessage()
183
+ } else if (step.type === 'step') {
184
+ updateStepMessage(step)
154
185
  }
155
-
156
- return hasPlanningAwaiting ? true : (step.type !== 'planning' && step.type !== 'step' &&
157
- (step.type !== 'answer' || (step.type === 'answer' && step.status === 'pending')))
158
186
  })
159
187
  }
160
188
 
161
- if (value.agent_info?.type === 'tool') {
189
+ if (value.agent_info?.type === 'tool' && value.agent_info?.action !== 'awaiting_approval') {
162
190
  updateToolStatus(value.agent_info)
163
191
  }
164
192
 
193
+ if (value.steps) {
194
+ const tool = findLast(value.steps, (item) => item.type === 'tool')
195
+ if (tool && tool.status === 'running') {
196
+ const messageId = planningToolDictionaryHelper.getMessageIdPlanningStepFromToolExecutionId(tool.id)
197
+ const originalItem = messages.find((message) => `${message.id}` === messageId)
198
+ const originalItemValue = originalItem?.getValue()
199
+ let update = false
200
+ const step = originalItemValue?.steps?.find(step => step.type === 'step')
201
+ if (step && step.attempts?.[0]?.tools?.[0]?.executionId === tool.id) {
202
+ step.attempts?.forEach((attempt, i) => {
203
+ const newAttempt = tool.attempts?.[i]
204
+ if (!newAttempt) return
205
+ attempt.tools = attempt.tools?.map((origTool, j) => {
206
+ const newTool = newAttempt.tools?.[j]
207
+ if (!newTool || origTool?.executionId !== newTool?.executionId) return origTool
208
+ update = true
209
+ return { ...origTool, ...newTool }
210
+ })
211
+ })
212
+ if (step.attempts.length < tool.attempts.length) {
213
+ step.attempts.push(tool.attempts[tool.attempts.length - 1])
214
+ }
215
+ }
216
+ if (update) {
217
+ originalItem?.setValue({ ...originalItemValue as TextChatEntry })
218
+ }
219
+ }
220
+ }
221
+
165
222
  botEntry.setValue(createEntryValueFromChatResponse(value, knowledgeSources, chat.get('agent')))
166
223
  })
167
224
 
168
225
  let finalValue: Partial<ChatResponse3> | undefined
169
226
  try {
170
227
  finalValue = await stream.getValue()
228
+
229
+ const lastPlanningAwaiting = getLastPlanningAwaiting()
230
+ if (lastPlanningAwaiting) {
231
+ const value = lastPlanningAwaiting.getValue()
232
+ value.content = finalValue.answer || value.content
233
+ lastPlanningAwaiting.setValue(value)
234
+ }
171
235
  // if the streaming feature is not enabled, we only add the chat entry once the streaming has finished
172
236
  if (!chat.get('features').streaming) {
173
237
  chat.pushMessage(botEntry)
package/src/layout.css CHANGED
@@ -184,3 +184,9 @@
184
184
  pointer-events: auto;
185
185
  color: var(--light-contrastText)
186
186
  }
187
+
188
+ .menu-citric-hr {
189
+ hr {
190
+ margin: 0;
191
+ }
192
+ }
@@ -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
  }
@@ -59,6 +59,10 @@ export interface ChatPropertiesWithOptionalFeatures {
59
59
  * If a feature is marked as false, it's disabled, otherwise it's enabled.
60
60
  */
61
61
  features?: Partial<ChatFeatures>,
62
+ /**
63
+ * The current LLM (Large Language Model) being used for this chat.
64
+ */
65
+ selected_model_id?: string,
62
66
  }
63
67
 
64
68
  export interface ChatProperties extends ChatPropertiesWithOptionalFeatures {
package/src/utils/chat.ts CHANGED
@@ -26,5 +26,6 @@ export function buildConversationContext(state: ChatState, message?: ChatEntry):
26
26
  platform: 'web-widget',
27
27
  platform_version: navigator.userAgent,
28
28
  stackspot_ai_version: appData.version,
29
+ selected_model_id: state.get('selected_model_id'),
29
30
  }
30
31
  }
@@ -1,4 +1,4 @@
1
- import { DocumentResponse, SourceKnowledgeSource, SourceProjectFile4, SourceStackAi } from '@stack-spot/portal-network/api/ai'
1
+ import { DocumentResponse, SourceKnowledgeSource, SourceProjectFile3, SourceStackAi } from '@stack-spot/portal-network/api/ai'
2
2
  import { KnowledgeSource } from '../state/ChatEntry'
3
3
 
4
4
  /**
@@ -60,7 +60,7 @@ export function extractCodeFromKSDocument(document: DocumentResponse): { languag
60
60
  * @returns a list of knowledge sources in the format expected by the chat.
61
61
  */
62
62
  export function genericSourcesToKnowledgeSources(
63
- sources: (SourceStackAi | SourceKnowledgeSource | SourceProjectFile4)[] | undefined,
63
+ sources: (SourceStackAi | SourceKnowledgeSource | SourceProjectFile3)[] | undefined,
64
64
  ): KnowledgeSource[] | undefined {
65
65
  return sources?.filter(s => s.type === 'knowledge_source').map(ks => {
66
66
  const { document_id: documentId, document_score: documentScore, name, slug } = ks as SourceKnowledgeSource
@@ -2,6 +2,7 @@ class PlanningToolDictionaryHelper {
2
2
  static instance: PlanningToolDictionaryHelper | undefined
3
3
  private toolExecutionIdPlanningStep: Record<string, string> = {}
4
4
  private toolExecutionIdToolStep: Record<string, string> = {}
5
+ private stepId: Record<string, number> = {}
5
6
 
6
7
  private constructor() {
7
8
  PlanningToolDictionaryHelper.instance = this
@@ -18,6 +19,14 @@ class PlanningToolDictionaryHelper {
18
19
  setMessageIdToolStepToolExecutionId(messageId: string, toolExecutionId: string){
19
20
  this.toolExecutionIdToolStep[toolExecutionId] = messageId
20
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
+ }
21
30
 
22
31
  getMessageIdPlanningStepFromToolExecutionId(toolExecutionId: string){
23
32
  return this.toolExecutionIdPlanningStep[toolExecutionId]
@@ -19,8 +19,7 @@ export const AgentDescriptionBox = styled.div`
19
19
  color: ${theme.color.light[700]};
20
20
  line-height: 18px;
21
21
  background-color: ${theme.color.light[400]};
22
- margin: -10px;
23
- padding: 8px;
22
+ margin-top: 16px;
24
23
 
25
24
  section {
26
25
  padding-bottom: 10px;
@@ -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, useCurrentChatState, 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, StepsPlaceholder } from './StepsList'
20
+ import { StepsList, StepsPlaceholder, ViewToolsDetails } from './StepsList'
21
21
 
22
22
  export interface CustomRenderResult {
23
23
  /**
@@ -69,12 +69,12 @@ interface Props extends CustomMessage {
69
69
  isLast: boolean,
70
70
  }
71
71
 
72
- interface RenderInputsEntryProps {
73
- isLast: boolean,
74
- entry: TextChatEntry,
75
- value: string[],
72
+ interface RenderInputsEntryProps {
73
+ isLast: boolean,
74
+ entry: TextChatEntry,
75
+ value: string[],
76
76
  setValue: Dispatch<React.SetStateAction<string[]>>,
77
- labels: string[],
77
+ labels: string[],
78
78
  setLabels: Dispatch<React.SetStateAction<string[]>>,
79
79
  }
80
80
 
@@ -113,7 +113,7 @@ const RenderInputsEntry = ({ isLast, entry, value, setValue, labels, setLabels }
113
113
  <Input name={entry.name} onChange={v => setValue([v])} required style={{ height: '30px', width: '33%' }} />}
114
114
  </Row>
115
115
  )}
116
- />
116
+ />
117
117
  }
118
118
 
119
119
  if (entry.type === 'button-list') {
@@ -145,7 +145,7 @@ const RenderInputsEntry = ({ isLast, entry, value, setValue, labels, setLabels }
145
145
  renderLabel={o => (
146
146
  <Row gap={3}>
147
147
  <Text>{o.label}</Text>
148
- {o.hasInput && o.value && labels.findIndex((label) => label === o.value)!== -1 &&
148
+ {o.hasInput && o.value && labels.findIndex((label) => label === o.value) !== -1 &&
149
149
  <div style={{ width: '33%' }}>
150
150
  <Input
151
151
  name={entry.name}
@@ -158,7 +158,7 @@ const RenderInputsEntry = ({ isLast, entry, value, setValue, labels, setLabels }
158
158
  const newValue = [...value]
159
159
  newValue[customIndex] = v
160
160
  setValue(newValue)
161
- }
161
+ }
162
162
  }}
163
163
  required={true}
164
164
  style={{ height: '30px' }}
@@ -220,6 +220,19 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
220
220
  const [showUserButtonCopy, setShowUserButtonCopy] = useState(false)
221
221
  const isPlanning = useCurrentChatState('isPlaning') ?? false
222
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
+
223
236
  useChatScrollToBottomEffect(ref, [entry])
224
237
  useMidnightUpdateView()
225
238
 
@@ -264,7 +277,7 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
264
277
  }
265
278
  }
266
279
 
267
- const renderActions = useCallback(()=> <> {entry.actions?.length && (
280
+ const renderActions = useCallback(() => <> {entry.actions?.length && (
268
281
  <div className="actions">
269
282
  {entry.actions.map(
270
283
  (a, index) => (<>
@@ -344,7 +357,8 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
344
357
  widget.set('panel', 'resources')
345
358
  }
346
359
 
347
- return (entry.content || entry.error || !!entry.steps?.length || entry.upload?.length || isPlanning) && (
360
+ return (entry.content || entry.error || !!entry.steps?.length ||
361
+ entry.upload?.length) && (!isMessageHidden || !toolsStep || isPlanning) && (
348
362
  <li key={entry.messageId} className={entry.agentType} ref={ref}>
349
363
  <div className="chat-message-container"
350
364
  onMouseEnter={entry.agentType === 'user' ? () => setShowUserButtonCopy(true) : undefined}
@@ -357,32 +371,19 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
357
371
  {!!entry.badges?.length && <div className="badges">
358
372
  {entry.badges.map((b, index) => <Badge key={index} colorPalette={b.color ?? 'cyan'} appearance="square">{b.label}</Badge>)}
359
373
  </div>}
360
-
361
- {!!entry.steps?.length && <StepsList steps={entry.steps} chatId={chat.id} messageId={message.id} />}
374
+
375
+ {!!entry.steps?.length && <StepsList steps={entry.steps} chatId={chat.id} messageId={message.id}
376
+ userHasAlreadyAnswered={userHasAlreadyAnswered} />}
362
377
 
363
378
  {renderContent()}
364
-
379
+
365
380
  </div>
366
381
  )}
367
- {isPlanning && entry.agentType === 'bot' && isLast && <StepsPlaceholder /> }
368
-
382
+ {isPlanning && entry.agentType === 'bot' && isLast && <StepsPlaceholder />}
383
+
369
384
  {entry.error && <Alert type="error">{entry.error}</Alert>}
370
385
  </div>
371
386
  {afterMessage && createElement(afterMessage, { message })}
372
- {/* {!!entry.tools?.length && <StackedBadge
373
- aria-label={t.openToolsPanel}
374
- title={t.openToolsPanel}
375
- tabIndex={0}
376
- role="button"
377
- className="tools-badge"
378
- label={t.tools}
379
- images={entry.tools.slice(0, 3).map((id) => {
380
- const tool = toolById(id, toolKits)
381
- return { key: id, name: tool?.name || id, icon: <Icon icon="Cog" />, url: tool?.image }
382
- })}
383
- onClick={openToolsPanel}
384
- style={{ marginTop: '12px', width: 'fit-content' }}
385
- />} */}
386
387
 
387
388
  {!!entry.knowledgeSources?.length && <div className="ks-box">
388
389
  <Text appearance="microtext1" color="light.700">Knowledge Sources:</Text>
@@ -393,35 +394,40 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
393
394
  ))}</ul>
394
395
  </div>}
395
396
 
396
- {(!!agentsTools?.length || !!entry.tools?.length) && <div className="tools-box">
397
- <Button appearance="none" onClick={openResourcesPanel} aria-label={t.openResourcesPanel}>
398
- {agentsTools?.map((agent) => (
399
- <ImageBox key={agent.id} className="agent-info-avatar-resource">
400
- <ImageWithFallback
401
- src={agent.avatar ?? undefined}
402
- fallback={<Icon icon="Agent" />}
403
- alt={agent.name}
404
- aria-label={agent.name}
405
- title={agent.name}
406
- />
407
- </ImageBox>
408
- ))}
409
- {entry.tools?.map((id) => {
410
- const tool = toolById(id, toolKits)
411
- return (
412
- <ImageBox key={id} className="agent-info-avatar-resource">
397
+ {(!!agentsTools?.length || !!entry.tools?.length) &&
398
+ <div className="tools-box">
399
+ <Button appearance="none" onClick={openResourcesPanel} aria-label={t.openResourcesPanel}>
400
+ {agentsTools?.map((agent) => (
401
+ <ImageBox key={agent.id} className="agent-info-avatar-resource">
413
402
  <ImageWithFallback
414
- src={tool?.image}
415
- fallback={<Icon icon="Cog" />}
416
- alt={tool?.name}
417
- aria-label={tool?.name}
418
- title={tool?.name}
403
+ src={agent.avatar ?? undefined}
404
+ fallback={<Icon icon="Agent" />}
405
+ alt={agent.name}
406
+ aria-label={agent.name}
407
+ title={agent.name}
419
408
  />
420
409
  </ImageBox>
421
- )})}
422
- </Button>
423
- </div>}
424
-
410
+ ))}
411
+ {entry?.tools?.map((id) => {
412
+ if (agentsTools?.some((agent) => agent.id === id)) return null
413
+ const tool = toolById(id, toolKits)
414
+ return (
415
+ <ImageBox key={id} className="agent-info-avatar-resource">
416
+ <ImageWithFallback
417
+ src={tool?.image}
418
+ fallback={<Icon icon="Cog" />}
419
+ alt={tool?.name}
420
+ aria-label={tool?.name}
421
+ title={tool?.name}
422
+ />
423
+ </ImageBox>
424
+ )
425
+ })}
426
+ </Button>
427
+ <ViewToolsDetails chatId={chat.id} />
428
+ </div>
429
+ }
430
+
425
431
  {shouldShowFooter && <div className="message-footer">
426
432
  {entry.agentType === 'bot' && !entry.error && <div className="message-actions">
427
433
  {entry.type === 'md' && (