@stack-spot/ai-chat-widget 2.3.0 → 2.3.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 (56) hide show
  1. package/CHANGELOG.md +109 -8
  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 +8 -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 +23 -8
  8. package/dist/chat-interceptors/send-message.js.map +1 -1
  9. package/dist/state/ChatState.d.ts +4 -0
  10. package/dist/state/ChatState.d.ts.map +1 -1
  11. package/dist/state/ChatState.js.map +1 -1
  12. package/dist/utils/chat.d.ts.map +1 -1
  13. package/dist/utils/chat.js +1 -0
  14. package/dist/utils/chat.js.map +1 -1
  15. package/dist/utils/knowledge-source.d.ts +2 -2
  16. package/dist/views/Chat/ChatMessage.d.ts.map +1 -1
  17. package/dist/views/Chat/ChatMessage.js +1 -1
  18. package/dist/views/Chat/ChatMessage.js.map +1 -1
  19. package/dist/views/Chat/StepsList.d.ts.map +1 -1
  20. package/dist/views/Chat/StepsList.js +9 -8
  21. package/dist/views/Chat/StepsList.js.map +1 -1
  22. package/dist/views/MessageInput/ButtonBar.d.ts.map +1 -1
  23. package/dist/views/MessageInput/ButtonBar.js +2 -1
  24. package/dist/views/MessageInput/ButtonBar.js.map +1 -1
  25. package/dist/views/MessageInput/ModelSwitcher/index.d.ts +2 -0
  26. package/dist/views/MessageInput/ModelSwitcher/index.d.ts.map +1 -0
  27. package/dist/views/MessageInput/ModelSwitcher/index.js +25 -0
  28. package/dist/views/MessageInput/ModelSwitcher/index.js.map +1 -0
  29. package/dist/views/MessageInput/ModelSwitcher/utils.d.ts +30 -0
  30. package/dist/views/MessageInput/ModelSwitcher/utils.d.ts.map +1 -0
  31. package/dist/views/MessageInput/ModelSwitcher/utils.js +91 -0
  32. package/dist/views/MessageInput/ModelSwitcher/utils.js.map +1 -0
  33. package/dist/views/MessageInput/dictionary.d.ts +1 -1
  34. package/dist/views/MessageInput/dictionary.d.ts.map +1 -1
  35. package/dist/views/MessageInput/dictionary.js +6 -0
  36. package/dist/views/MessageInput/dictionary.js.map +1 -1
  37. package/dist/views/MessageInput/styled.d.ts +12 -0
  38. package/dist/views/MessageInput/styled.d.ts.map +1 -1
  39. package/dist/views/MessageInput/styled.js +35 -0
  40. package/dist/views/MessageInput/styled.js.map +1 -1
  41. package/dist/views/Resources.js.map +1 -1
  42. package/package.json +4 -4
  43. package/src/app-metadata.json +6 -6
  44. package/src/chat-interceptors/quick-commands.ts +10 -3
  45. package/src/chat-interceptors/send-message.ts +31 -12
  46. package/src/state/ChatState.ts +4 -0
  47. package/src/utils/chat.ts +1 -0
  48. package/src/utils/knowledge-source.ts +2 -2
  49. package/src/views/Chat/ChatMessage.tsx +23 -22
  50. package/src/views/Chat/StepsList.tsx +13 -10
  51. package/src/views/MessageInput/ButtonBar.tsx +2 -0
  52. package/src/views/MessageInput/ModelSwitcher/index.tsx +67 -0
  53. package/src/views/MessageInput/ModelSwitcher/utils.tsx +143 -0
  54. package/src/views/MessageInput/dictionary.ts +6 -0
  55. package/src/views/MessageInput/styled.ts +37 -0
  56. package/src/views/Resources.tsx +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stack-spot/ai-chat-widget",
3
- "version": "2.3.0",
3
+ "version": "2.3.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.3",
14
+ "@stack-spot/citric-icons": "^0.2.5",
15
15
  "@stack-spot/portal-theme": "^1.2.1",
16
16
  "@citric/core": "^6.4.0",
17
- "@stack-spot/portal-components": "^2.26.0",
17
+ "@stack-spot/portal-components": "^2.27.3",
18
18
  "@citric/icons": "^5.13.0",
19
- "@stack-spot/portal-network": "0.193.0",
19
+ "@stack-spot/portal-network": "0.190.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.3.0",
4
- "date": "Tue Oct 28 2025 17:04:35 GMT+0000 (Coordinated Universal Time)",
3
+ "version": "2.3.1-beta.1",
4
+ "date": "Thu Nov 06 2025 13:51:17 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.193.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.190.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",
@@ -245,9 +245,16 @@ export function createQuickCommandInterceptor(widget: WidgetState, getEditor: ()
245
245
  await (currentStep.type === 'FETCH' ? runFetchStep(ctx, currentIndex) : runLLMStep(ctx, currentIndex))
246
246
 
247
247
  let nextIndex = currentIndex + 1
248
- if (currentStep.next_step_slug) {
249
- nextIndex = currentStep.next_step_slug === 'end' ?
250
- 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)
251
258
  }
252
259
  await runStepsRecursively(nextIndex, progress, ctx, iteration)
253
260
  }
@@ -24,7 +24,7 @@ function createEntryValueFromChatResponse(
24
24
  agent: LabeledWithImage | undefined,
25
25
  includeDate = false,
26
26
  ): TextChatEntry {
27
- const entry= {
27
+ const entry = {
28
28
  agentType: 'bot',
29
29
  type: 'md',
30
30
  content: response.answer ?? '',
@@ -65,9 +65,10 @@ export async function sendMessageInterceptor(entry: ChatEntry, chat: ChatState,
65
65
  chat.untitled = false
66
66
  }
67
67
 
68
- //Verify if the last planning in the messages has status awaiting_approval
69
68
  const messages = chat.getMessages()
70
- const lastPlanningAwaiting = findLast(messages, item => {
69
+
70
+ //Verify if the last planning in the messages has status awaiting_approval
71
+ const getLastPlanningAwaiting = () => findLast(messages, item => {
71
72
  const steps = item.getValue().steps
72
73
  if (steps) {
73
74
  const hasPlanning = steps.find((step) => step.type === 'planning')
@@ -86,6 +87,8 @@ export async function sendMessageInterceptor(entry: ChatEntry, chat: ChatState,
86
87
  let knowledgeSources: KnowledgeSource[] | undefined
87
88
 
88
89
  const updatePlanningMessage = () => {
90
+ const lastPlanningAwaiting = getLastPlanningAwaiting()
91
+
89
92
  if (lastPlanningAwaiting) {
90
93
  const originalItem = messages.find((message) => message.id === lastPlanningAwaiting.id)
91
94
  const originalItemValue = originalItem?.getValue()
@@ -97,8 +100,10 @@ export async function sendMessageInterceptor(entry: ChatEntry, chat: ChatState,
97
100
  originalItem?.setValue({ ...originalItemValue as TextChatEntry })
98
101
  }
99
102
  }
100
-
103
+
101
104
  const updateStepMessage = (step: ChatStep) => {
105
+ const lastPlanningAwaiting = getLastPlanningAwaiting()
106
+
102
107
  if (lastPlanningAwaiting) {
103
108
  const originalItem = messages.find((message) => message.id === lastPlanningAwaiting.id)
104
109
  const originalItemValue = originalItem?.getValue()
@@ -120,7 +125,8 @@ export async function sendMessageInterceptor(entry: ChatEntry, chat: ChatState,
120
125
  const originalItemValue = originalItem?.getValue()
121
126
  let update = false
122
127
  const status = agentInfo.action === 'start' ? 'running' : 'success'
123
- const step = originalItemValue?.steps?.find(step => step.type === 'step' && step.attempts?.[0].tools?.[0].executionId === executionId)
128
+ const step = originalItemValue?.steps
129
+ ?.find(step => step.type === 'step' && step.attempts?.[0]?.tools?.[0]?.executionId === executionId)
124
130
  if (step && step.status !== status) {
125
131
  step.status = status
126
132
  update = true
@@ -135,8 +141,8 @@ export async function sendMessageInterceptor(entry: ChatEntry, chat: ChatState,
135
141
  const toolMessageId = planningToolDictionaryHelper.getMessageIdToolStepFromToolExecutionId(executionId)
136
142
  const toolOriginalItem = messages.find((message) => `${message.id}` === toolMessageId)
137
143
  const toolOriginalItemValue = toolOriginalItem?.getValue()
138
- const toolStep = toolOriginalItemValue?.steps?.find(step =>
139
- 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)
140
146
  update = false
141
147
  if (toolOriginalItemValue && toolStep && toolStep.status !== status) {
142
148
  toolOriginalItemValue.steps = undefined
@@ -157,10 +163,19 @@ export async function sendMessageInterceptor(entry: ChatEntry, chat: ChatState,
157
163
  }
158
164
  }
159
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
+
160
174
  if (value.sources?.length !== knowledgeSources?.length && chat.get('features').showSourcesInResponse) {
161
175
  knowledgeSources = genericSourcesToKnowledgeSources(value.sources)
162
176
  }
163
177
 
178
+ const lastPlanningAwaiting = getLastPlanningAwaiting()
164
179
  if (lastPlanningAwaiting && value.steps) {
165
180
  value.steps?.map(step => {
166
181
  if (step.type === 'planning') {
@@ -177,24 +192,26 @@ export async function sendMessageInterceptor(entry: ChatEntry, chat: ChatState,
177
192
 
178
193
  if (value.steps) {
179
194
  const tool = findLast(value.steps, (item) => item.type === 'tool')
180
-
181
195
  if (tool && tool.status === 'running') {
182
196
  const messageId = planningToolDictionaryHelper.getMessageIdPlanningStepFromToolExecutionId(tool.id)
183
197
  const originalItem = messages.find((message) => `${message.id}` === messageId)
184
198
  const originalItemValue = originalItem?.getValue()
185
199
  let update = false
186
200
  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) => {
201
+ if (step && step.attempts?.[0]?.tools?.[0]?.executionId === tool.id) {
202
+ step.attempts?.forEach((attempt, i) => {
189
203
  const newAttempt = tool.attempts?.[i]
190
204
  if (!newAttempt) return
191
- attempt.tools?.map((origTool, j) => {
205
+ attempt.tools = attempt.tools?.map((origTool, j) => {
192
206
  const newTool = newAttempt.tools?.[j]
193
- if (!newTool || origTool.executionId !== newTool.executionId) return origTool
207
+ if (!newTool || origTool?.executionId !== newTool?.executionId) return origTool
194
208
  update = true
195
209
  return { ...origTool, ...newTool }
196
210
  })
197
211
  })
212
+ if (step.attempts.length < tool.attempts.length) {
213
+ step.attempts.push(tool.attempts[tool.attempts.length - 1])
214
+ }
198
215
  }
199
216
  if (update) {
200
217
  originalItem?.setValue({ ...originalItemValue as TextChatEntry })
@@ -208,6 +225,8 @@ export async function sendMessageInterceptor(entry: ChatEntry, chat: ChatState,
208
225
  let finalValue: Partial<ChatResponse3> | undefined
209
226
  try {
210
227
  finalValue = await stream.getValue()
228
+
229
+ const lastPlanningAwaiting = getLastPlanningAwaiting()
211
230
  if (lastPlanningAwaiting) {
212
231
  const value = lastPlanningAwaiting.getValue()
213
232
  value.content = finalValue.answer || value.content
@@ -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
@@ -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' }}
@@ -219,7 +219,7 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
219
219
  const [copied, setCopied] = useState(false)
220
220
  const [showUserButtonCopy, setShowUserButtonCopy] = useState(false)
221
221
  const isPlanning = useCurrentChatState('isPlaning') ?? false
222
-
222
+
223
223
  // when we have a steps but we are not showing any content of the step
224
224
  // (because it is a tool and the user has already answered the question)
225
225
  // we do not want to show an avatar with empty content, so we hide the entire message
@@ -227,12 +227,12 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
227
227
  const messages = useChatMessages(chat.id)
228
228
  const userHasAlreadyAnswered = useMemo(() => {
229
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()
230
+ if (messages.length - 1 === messageIndex) return false
231
+ const nextMessage = messages[messageIndex + 1].getValue()
232
232
  return nextMessage.agentType === 'user'
233
233
  }, [messages, messages.length])
234
234
  const isMessageHidden = toolsStep && userHasAlreadyAnswered
235
-
235
+
236
236
  useChatScrollToBottomEffect(ref, [entry])
237
237
  useMidnightUpdateView()
238
238
 
@@ -277,7 +277,7 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
277
277
  }
278
278
  }
279
279
 
280
- const renderActions = useCallback(()=> <> {entry.actions?.length && (
280
+ const renderActions = useCallback(() => <> {entry.actions?.length && (
281
281
  <div className="actions">
282
282
  {entry.actions.map(
283
283
  (a, index) => (<>
@@ -357,7 +357,7 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
357
357
  widget.set('panel', 'resources')
358
358
  }
359
359
 
360
- return (entry.content || entry.error || !!entry.steps?.length ||
360
+ return (entry.content || entry.error || !!entry.steps?.length ||
361
361
  entry.upload?.length) && (!isMessageHidden || !toolsStep || isPlanning) && (
362
362
  <li key={entry.messageId} className={entry.agentType} ref={ref}>
363
363
  <div className="chat-message-container"
@@ -371,16 +371,16 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
371
371
  {!!entry.badges?.length && <div className="badges">
372
372
  {entry.badges.map((b, index) => <Badge key={index} colorPalette={b.color ?? 'cyan'} appearance="square">{b.label}</Badge>)}
373
373
  </div>}
374
-
375
- {!!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
376
  userHasAlreadyAnswered={userHasAlreadyAnswered} />}
377
377
 
378
378
  {renderContent()}
379
-
379
+
380
380
  </div>
381
381
  )}
382
- {isPlanning && entry.agentType === 'bot' && isLast && <StepsPlaceholder /> }
383
-
382
+ {isPlanning && entry.agentType === 'bot' && isLast && <StepsPlaceholder />}
383
+
384
384
  {entry.error && <Alert type="error">{entry.error}</Alert>}
385
385
  </div>
386
386
  {afterMessage && createElement(afterMessage, { message })}
@@ -421,7 +421,7 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
421
421
  />
422
422
  </ImageBox>
423
423
  ))}
424
- {entry.tools?.map((id) => {
424
+ {entry?.tools?.map((id) => {
425
425
  const tool = toolById(id, toolKits)
426
426
  return (
427
427
  <ImageBox key={id} className="agent-info-avatar-resource">
@@ -433,11 +433,12 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
433
433
  title={tool?.name}
434
434
  />
435
435
  </ImageBox>
436
- )})}
436
+ )
437
+ })}
437
438
  </Button>
438
439
  <ViewToolsDetails chatId={chat.id} />
439
440
  </div>}
440
-
441
+
441
442
  {shouldShowFooter && <div className="message-footer">
442
443
  {entry.agentType === 'bot' && !entry.error && <div className="message-actions">
443
444
  {entry.type === 'md' && (
@@ -189,7 +189,7 @@ export const ToolStepsList = ({ toolStep, messageId, chatId }: { toolStep: ToolC
189
189
 
190
190
  useEffect(() => {
191
191
  if (!toolStep) return undefined
192
- const executionId = toolStep.attempts?.[0].tools?.[0].executionId
192
+ const executionId = toolStep.attempts?.[0]?.tools?.[0]?.executionId
193
193
  if (!executionId) return
194
194
 
195
195
  updateToolStep(messages, executionId, toolStep.status)
@@ -251,7 +251,7 @@ export const StepsList = ({ steps, messageId, chatId, userHasAlreadyAnswered }:
251
251
 
252
252
  useEffect(() => {
253
253
  actualSteps.map((item) => {
254
- const executionId = item.attempts[0]?.tools?.[0].executionId
254
+ const executionId = item.attempts[0]?.tools?.[0]?.executionId
255
255
  if (executionId) {
256
256
  planningToolDictionaryHelper.setMessageIdPlanningStepToolExecutionId(`${messageId}`, executionId)
257
257
  }
@@ -271,8 +271,8 @@ export const StepsList = ({ steps, messageId, chatId, userHasAlreadyAnswered }:
271
271
  const isLastStepDone = actualSteps[actualSteps.length - 1]?.status !== 'running' &&
272
272
  actualSteps[actualSteps.length - 1]?.status !== 'pending'
273
273
  const totalTools = useMemo(() => actualSteps?.reduce((sum, step) => {
274
- const firstAttempt = step.attempts && step.attempts[0]
275
- const toolsCount = firstAttempt.tools?.length ?? 0
274
+ const firstAttempt = !!step.attempts && !!step.attempts.length ? step.attempts[0] : undefined
275
+ const toolsCount = firstAttempt?.tools?.length ?? 0
276
276
  return sum + toolsCount
277
277
  }, 0), [steps])
278
278
 
@@ -302,13 +302,15 @@ export const StepsList = ({ steps, messageId, chatId, userHasAlreadyAnswered }:
302
302
  <Column gap="8px" mt="8px">
303
303
  <Divider colorScheme="light" />
304
304
  <Text color="light.700">{planning?.[0]?.user_question}</Text>
305
- {!userHasAlreadyAnswered && planning?.[0]?.status === 'awaiting_approval' && <AwaitingApproval chatId={chatId} />}
305
+ {!userHasAlreadyAnswered && planning?.[0]?.status === 'awaiting_approval' && <AwaitingApproval chatId={chatId} />}
306
306
  </Column>
307
307
 
308
+ {userHasAlreadyAnswered && planning?.[0]?.status === 'success' && <ViewToolsDetails chatId={chatId} />}
309
+
308
310
  </div>
309
311
  </AnimatedHeight> : null}
310
312
 
311
- {toolsStep && toolsStep.status === 'awaiting_approval' && !userHasAlreadyAnswered &&
313
+ {toolsStep && toolsStep.status === 'awaiting_approval' && !userHasAlreadyAnswered &&
312
314
  <ToolStepsList toolStep={toolsStep} messageId={messageId} chatId={chatId} />}
313
315
  </>
314
316
  )
@@ -317,18 +319,19 @@ export const StepsList = ({ steps, messageId, chatId, userHasAlreadyAnswered }:
317
319
  export const ViewToolsDetails = ({ chatId }: { chatId: string }) => {
318
320
  const t = useTranslate(dictionary)
319
321
  const messages = useChatMessages(chatId)
320
- const messageId = useMemo(() => {
322
+ const widget = useWidget()
323
+ const getPlanningMessageId = () => {
321
324
  const messageWithPlanning = findLast(messages, (message) => {
322
325
  const steps = message.getValue().steps
323
326
  const planningStep = steps?.find((step) => step.type === 'planning' && step.status === 'success')
324
327
  return planningStep ? true : false
325
328
  })
326
329
  return messageWithPlanning?.id
327
-
328
- }, [messages])
329
- const widget = useWidget()
330
+ }
330
331
 
331
332
  function openToolsPanel() {
333
+ const messageId = getPlanningMessageId()
334
+
332
335
  if (messageId) {
333
336
  widget.set('currentMessageInPanel', { chatId, messageId })
334
337
  widget.set('panel', 'steps')
@@ -1,6 +1,7 @@
1
1
  import { IconButton, Row } from '@stack-spot/citric-react'
2
2
  import { useCurrentChat, useCurrentChatState, useWidget } from '../../context/hooks'
3
3
  import { useMessageInputDictionary } from './dictionary'
4
+ import { ModelSwitcher } from './ModelSwitcher'
4
5
  import { SelectContent } from './SelectContent'
5
6
  import { SelectionBarWrapper } from './styled'
6
7
 
@@ -29,6 +30,7 @@ export const ButtonBar = ({ onSend, isLoading }: SelectionBarProps) => {
29
30
  <IconButton icon="Code" appearance="square" aria-label={t.code} title={t.code} onClick={() => widget.set('panel', 'editor')} />
30
31
  )}
31
32
  </Row>
33
+ <ModelSwitcher />
32
34
  {isLoading ? (
33
35
  <IconButton
34
36
  icon="Stop"
@@ -0,0 +1,67 @@
1
+ import { CitricIconOutline, CitricIconSocial } from '@stack-spot/citric-icons'
2
+ import { AsyncContent, Button, Column, FieldGroup, Icon, Input, Row } from '@stack-spot/citric-react'
3
+ import { SelectionList } from '@stack-spot/portal-components/SelectionList'
4
+ import { agentToolsClient, genAiInferenceClient } from '@stack-spot/portal-network'
5
+ import { useMemo, useState } from 'react'
6
+ import { CSSProperties } from 'styled-components'
7
+ import { useCurrentChat, useCurrentChatState } from '../../../context/hooks'
8
+ import { useMessageInputDictionary } from '../dictionary'
9
+ import { RowWrapperStyled, stylesModelSwitcher } from '../styled'
10
+ import { getModelData, handleFilter, providerIcon } from './utils'
11
+
12
+ export const ModelSwitcher = () => {
13
+ const t = useMessageInputDictionary()
14
+ const agentCurrentChat = useCurrentChatState('agent')
15
+ const chat = useCurrentChat()
16
+ const [visibleMenu, setVisibleMenu] = useState(false)
17
+ const [agentData, isLoadingAgentData] = agentToolsClient.agent.useStatefulQuery({ agentId: agentCurrentChat?.id || '' })
18
+ const [listModels, isLoadingModels] = genAiInferenceClient.listModels.useStatefulQuery({ pageSize: 999, active: true })
19
+ const [filter, setFilter] = useState('')
20
+
21
+ const { modelName, modelProviderType, listItemsData } =
22
+ getModelData(chat, setVisibleMenu, agentData, listModels)
23
+
24
+ const data = useMemo(() => {
25
+ const items = listItemsData ?? []
26
+ return handleFilter(items, t, filter)
27
+ }, [filter, agentCurrentChat?.id, chat.get('selected_model_id'), listItemsData])
28
+
29
+ return (
30
+ <RowWrapperStyled>
31
+ <AsyncContent loading={isLoadingAgentData && isLoadingModels}>
32
+ <Button
33
+ className="button-select-model"
34
+ colorScheme="light"
35
+ size="sm"
36
+ aria-label={t.agent}
37
+ title={t.agent}
38
+ onClick={() => setVisibleMenu(state => !state)}
39
+ >
40
+ <Icon
41
+ icon={providerIcon[modelProviderType as CitricIconOutline | CitricIconSocial]}
42
+ group={modelProviderType === 'stackspot' ? 'outline' : 'social'}
43
+ />
44
+ {modelName}
45
+ <Icon icon="ChevronDown" group="fill" size="sm" />
46
+ </Button>
47
+ </AsyncContent>
48
+ <SelectionList
49
+ id="menuModelSwitcher"
50
+ items={data || []}
51
+ visible={visibleMenu}
52
+ onHide={() => setVisibleMenu(false)}
53
+ showListAsCard
54
+ style={stylesModelSwitcher.selection as CSSProperties}
55
+ before={
56
+ <Column>
57
+ <FieldGroup fullWidth style={{ marginTop: '8px' }}>
58
+ <Icon icon="Search" />
59
+ <Input type="search" value={filter} onChange={(value) => (setFilter(value))} />
60
+ </FieldGroup>
61
+ {!data.length ? <Row m="16px 8px">{t.nothingFound}</Row> : undefined}
62
+ </Column>
63
+ }
64
+ />
65
+ </RowWrapperStyled>
66
+ )
67
+ }