@stack-spot/ai-chat-widget 2.1.0-beta.1 → 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.
@@ -3,11 +3,11 @@ import { AnimatedHeight } from '@stack-spot/portal-components/AnimatedHeight'
3
3
  import { ChatStep, StepChatStep, ToolChatStep } from '@stack-spot/portal-network'
4
4
  import { theme } from '@stack-spot/portal-theme'
5
5
  import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
6
- import { findLastIndex } from 'lodash'
7
- import React, { useMemo } from 'react'
6
+ import { findLast, findLastIndex } from 'lodash'
7
+ import React, { useEffect, useMemo } from 'react'
8
8
  import styled from 'styled-components'
9
9
  import { Markdown } from '../../components/Markdown'
10
- import { useCurrentChat, useCurrentChatMessages, useWidget } from '../../context/hooks'
10
+ import { useChat, useChatMessages, useCurrentChat, useCurrentChatMessages, useWidget } from '../../context/hooks'
11
11
  import { ChatEntry } from '../../state/ChatEntry'
12
12
  import { planningToolDictionaryHelper } from '../../utils/planning-tool'
13
13
  import { updateToolStep } from '../../utils/update-tool-step'
@@ -17,6 +17,7 @@ interface Props {
17
17
  steps: ChatStep[],
18
18
  messageId: number,
19
19
  chatId: string,
20
+ userHasAlreadyAnswered?: boolean,
20
21
  }
21
22
 
22
23
  interface StepChatStepWithTarget extends Omit<StepChatStep, 'status' | 'id' | 'type'> {
@@ -52,14 +53,14 @@ const StepAccordionHeader = ({ step, index, expand }: Pick<StepProps, 'step' | '
52
53
  <Badge colorScheme="inverse" appearance="square">
53
54
  {t.step} {index}
54
55
  </Badge>}
55
- <Text className="step-title" appearance="body2" >
56
+ <Text className="step-title" appearance="body2">
56
57
  {step.input}
57
58
  </Text>
58
- {step.status === 'awaiting_approval' &&
59
- <Badge appearance="square" style={{ backgroundColor: theme.color.gray[800], color: theme.color.gray[50] }}>
60
- <Icon icon="Security" />
61
- {t.pendingReview}
62
- </Badge>}
59
+ {step.status === 'awaiting_approval' &&
60
+ <Badge appearance="square" style={{ backgroundColor: theme.color.gray[800], color: theme.color.gray[50] }}>
61
+ <Icon icon="Security" />
62
+ {t.pendingReview}
63
+ </Badge>}
63
64
  </Row>
64
65
  }
65
66
 
@@ -72,12 +73,13 @@ const StyledCard = styled(Card)`
72
73
  const Step = ({ step, index, onClick, total, totalTools, isAllDone }: StepProps) => {
73
74
  const t = useTranslate(dictionary)
74
75
  const status = getStatusIcon(step.status, isAllDone)
76
+ const hasTools = step.attempts?.[0]?.tools && step.attempts?.[0]?.tools?.length > 0
75
77
 
76
78
  return (
77
79
  <li tabIndex={onClick ? 0 : undefined} onClick={onClick} role={onClick ? 'button' : 'listitem'}>
78
80
  <Row gap="4px" alignItems="center">
79
81
  <div className="step-status-icon">{status}</div>
80
- <StyledCard p="8px" w="75%">
82
+ <StyledCard p="8px" w="80%">
81
83
  <Accordion header={expand => <StepAccordionHeader step={step} index={index} expand={expand} />}>
82
84
  <Column pt="12px">
83
85
  {total ?
@@ -90,27 +92,32 @@ const Step = ({ step, index, onClick, total, totalTools, isAllDone }: StepProps)
90
92
  <Row gap="4px">
91
93
  <Icon icon="BorderRadius" size="sm" color="light.700" />
92
94
  <Text color="light.700">{t.totalTools}</Text>
93
- {totalTools}
95
+ {totalTools ?? 0}
94
96
  </Row>
95
97
  </Row>
96
98
  : <>
97
- <Row pb="8px" gap="4px">
99
+ <Row pb="8px">
98
100
  <Icon icon="Target" size="sm" color="light.700" />
99
- <Text color="light.700">{t.stepGoal}:</Text>
100
- <Text>
101
+ <Text color="light.700" tag="span" style={{ margin: '0 4px' }}>{t.stepGoal}:</Text>
102
+ <Text tag="span">
101
103
  {step.input}
102
104
  </Text>
103
105
  </Row>
104
- <Row gap="4px">
106
+ {hasTools ? <>
107
+ <Row gap="4px">
108
+ <Icon icon="BorderRadius" size="sm" color="light.700" />
109
+ <Text color="light.700">{t.toolsToBeExecuted}:</Text>
110
+ </Row>
111
+ <ul className="tools-list">
112
+ {step.attempts?.[0]?.tools?.map((tool, index) => (<li key={`${tool.id}-${index}`}>
113
+ <Text>{tool.name}: {tool.goal}</Text>
114
+ </li>))}
115
+ </ul>
116
+ </> : <Row gap="4px">
105
117
  <Icon icon="BorderRadius" size="sm" color="light.700" />
106
- <Text color="light.700">{t.toolsToBeExecuted}:</Text>
118
+ <Text color="light.700">{t.noToolToBeUsed}</Text>
107
119
  </Row>
108
- <ul className="tools-list">
109
- {step.attempts?.[0]?.tools?.map((tool) => (<li key={tool.id}>
110
- <Text>{tool.name}: {tool.goal}</Text>
111
- </li>),
112
- )}
113
- </ul>
120
+ }
114
121
  </>
115
122
  }
116
123
  </Column>
@@ -140,9 +147,9 @@ export const StepsPlaceholder = () => {
140
147
  </Card>
141
148
  }
142
149
 
143
- const AwaitingApproval = ({ customApproveText }: { customApproveText?: string }) => {
150
+ const AwaitingApproval = ({ customApproveText, chatId }: { chatId: string, customApproveText?: string }) => {
144
151
  const t = useTranslate(dictionary)
145
- const chat = useCurrentChat()
152
+ const chat = useChat(chatId)
146
153
 
147
154
  const onAnswer = (response: string) => {
148
155
  chat.pushMessage(ChatEntry.createUserEntry('', false, undefined, undefined, response))
@@ -167,7 +174,7 @@ const AwaitingApproval = ({ customApproveText }: { customApproveText?: string })
167
174
  </>
168
175
  }
169
176
 
170
- export const ToolStepsList = ({ toolStep, messageId }: { toolStep: ToolChatStep, messageId: number }) => {
177
+ export const ToolStepsList = ({ toolStep, messageId, chatId }: { toolStep: ToolChatStep, messageId: number, chatId: string }) => {
171
178
  const t = useTranslate(dictionary)
172
179
  const chat = useCurrentChat()
173
180
  const messages = useCurrentChatMessages()
@@ -180,13 +187,13 @@ export const ToolStepsList = ({ toolStep, messageId }: { toolStep: ToolChatStep,
180
187
  return toolStep.attempts?.[0].tools?.[0]
181
188
  }, [toolStep])
182
189
 
183
- useMemo(() => {
190
+ useEffect(() => {
184
191
  if (!toolStep) return undefined
185
192
  const executionId = toolStep.attempts?.[0].tools?.[0].executionId
186
193
  if (!executionId) return
187
194
 
188
195
  updateToolStep(messages, executionId, toolStep.status)
189
-
196
+
190
197
  }, [messages, toolStep, toolStep.status])
191
198
 
192
199
  return <>
@@ -215,37 +222,52 @@ export const ToolStepsList = ({ toolStep, messageId }: { toolStep: ToolChatStep,
215
222
  </Markdown>
216
223
  </Accordion>
217
224
 
218
- <AwaitingApproval customApproveText={t.approveTool} />
225
+ <AwaitingApproval customApproveText={t.approveTool} chatId={chatId} />
219
226
  </Card>
220
227
  </div>
221
228
  </AnimatedHeight> : null}
222
229
  </>
223
230
  }
224
231
 
225
- export const StepsList = ({ steps, chatId, messageId }: Props) => {
232
+ export const StepsList = ({ steps, messageId, chatId, userHasAlreadyAnswered }: Props) => {
226
233
  const t = useTranslate(dictionary)
227
- const actualSteps = steps.filter(s => s.type === 'step')
234
+
235
+ const filteredSteps = steps.filter(s => s.type === 'step')
236
+ const actualSteps = useMemo(() => filteredSteps.filter((item) => {
237
+ const messageIdForStep = planningToolDictionaryHelper.getMessageIdFromStepId(item.id)
238
+ if (!messageIdForStep) {
239
+ planningToolDictionaryHelper.setMessageIdForStepId(messageId, item.id)
240
+ return true
241
+ } else if (messageIdForStep === messageId) {
242
+ return true
243
+ }
244
+ // If a step is from a planning and it is already inserted in the planningToolDictionaryHelper it means the step is already in a
245
+ // previous message and we do not want to show it again (for instance, when required a approval in the planning, we will have
246
+ // two messages with the same step id one for the planning awaiting and one for the planning end, so we want to show only one)
247
+ return false
248
+ }), [filteredSteps])
249
+
228
250
  const planning = steps.filter(s => s.type === 'planning')
229
-
230
- useMemo(() => {
251
+
252
+ useEffect(() => {
231
253
  actualSteps.map((item) => {
232
- const executionId = item.attempts[0]?.tools?.[0].executionId
254
+ const executionId = item.attempts[0]?.tools?.[0].executionId
233
255
  if (executionId) {
234
256
  planningToolDictionaryHelper.setMessageIdPlanningStepToolExecutionId(`${messageId}`, executionId)
235
257
  }
236
258
  })
237
259
  }, [actualSteps, messageId])
238
260
 
239
- const toolsStep = steps.find(s => s.type === 'tool')
240
-
241
- useMemo(() => {
261
+ const toolsStep = findLast(steps, s => s.type === 'tool')
262
+
263
+ useEffect(() => {
242
264
  const executionId = toolsStep?.attempts?.[0]?.tools?.[0]?.executionId
243
265
  if (executionId) {
244
266
  planningToolDictionaryHelper.setMessageIdToolStepToolExecutionId(`${messageId}`, executionId)
245
267
  }
246
268
  }, [toolsStep, messageId])
247
269
 
248
- const planningGoal = steps.filter(s => s.type === 'planning')?.[0]?.goal
270
+ const planningGoal = planning?.[0]?.goal
249
271
  const isLastStepDone = actualSteps[actualSteps.length - 1]?.status !== 'running' &&
250
272
  actualSteps[actualSteps.length - 1]?.status !== 'pending'
251
273
  const totalTools = useMemo(() => actualSteps?.reduce((sum, step) => {
@@ -256,12 +278,6 @@ export const StepsList = ({ steps, chatId, messageId }: Props) => {
256
278
 
257
279
  let currentStepIndex = findLastIndex(actualSteps, s => s.status === 'running' || s.status === 'success')
258
280
  if (currentStepIndex === -1) currentStepIndex = 0
259
- const widget = useWidget()
260
-
261
- function openToolsPanel() {
262
- widget.set('currentMessageInPanel', { chatId, messageId })
263
- widget.set('panel', 'steps')
264
- }
265
281
 
266
282
  return (<>
267
283
  {actualSteps.length > 0 ? <AnimatedHeight>
@@ -270,12 +286,10 @@ export const StepsList = ({ steps, chatId, messageId }: Props) => {
270
286
  <Icon icon="Target" size="sm" color="light.600" />
271
287
  <Text>{t.executionPlan}</Text>
272
288
  </Row>
289
+
273
290
  <ul className="steps-list">
274
- <Step
275
- step={actualSteps[currentStepIndex]}
276
- index={currentStepIndex + 1}
277
- />
278
-
291
+ {actualSteps.map((s, i) => <Step step={s} key={i} index={i + 1} />)}
292
+
279
293
  <Step
280
294
  step={{ status: 'target', input: planningGoal, attempts: [] }}
281
295
  index={currentStepIndex + 1}
@@ -288,23 +302,49 @@ export const StepsList = ({ steps, chatId, messageId }: Props) => {
288
302
  <Column gap="8px" mt="8px">
289
303
  <Divider colorScheme="light" />
290
304
  <Text color="light.700">{planning?.[0]?.user_question}</Text>
291
- {planning?.[0]?.status === 'awaiting_approval' && <AwaitingApproval />}
305
+ {!userHasAlreadyAnswered && planning?.[0]?.status === 'awaiting_approval' && <AwaitingApproval chatId={chatId} />}
292
306
  </Column>
293
307
 
294
- {isLastStepDone && <div className="step-actions">
295
- <Button colorScheme="light" size="sm" className="icon-button details" onClick={openToolsPanel}>
296
- <Icon group="fill" icon="Play" size="xs" />
297
- {t.detailed}
298
- </Button>
299
- </div>}
300
308
  </div>
301
309
  </AnimatedHeight> : null}
302
310
 
303
- {toolsStep && toolsStep.status === 'awaiting_approval' && <ToolStepsList toolStep={toolsStep} messageId={messageId} />}
311
+ {toolsStep && toolsStep.status === 'awaiting_approval' && !userHasAlreadyAnswered &&
312
+ <ToolStepsList toolStep={toolsStep} messageId={messageId} chatId={chatId} />}
304
313
  </>
305
314
  )
306
315
  }
307
316
 
317
+ export const ViewToolsDetails = ({ chatId }: { chatId: string }) => {
318
+ const t = useTranslate(dictionary)
319
+ const messages = useChatMessages(chatId)
320
+ const messageId = useMemo(() => {
321
+ const messageWithPlanning = findLast(messages, (message) => {
322
+ const steps = message.getValue().steps
323
+ const planningStep = steps?.find((step) => step.type === 'planning' && step.status === 'success')
324
+ return planningStep ? true : false
325
+ })
326
+ return messageWithPlanning?.id
327
+
328
+ }, [messages])
329
+ const widget = useWidget()
330
+
331
+ function openToolsPanel() {
332
+ if (messageId) {
333
+ widget.set('currentMessageInPanel', { chatId, messageId })
334
+ widget.set('panel', 'steps')
335
+ }
336
+ }
337
+
338
+ return <>
339
+ <div className="step-actions">
340
+ <Button colorScheme="light" size="sm" className="icon-button details" onClick={openToolsPanel}>
341
+ <Icon group="fill" icon="Play" size="xs" />
342
+ {t.detailed}
343
+ </Button>
344
+ </div>
345
+ </>
346
+ }
347
+
308
348
  const dictionary = {
309
349
  en: {
310
350
  step: 'Step',
@@ -324,6 +364,7 @@ const dictionary = {
324
364
  viewDetails: 'View details',
325
365
  approveTool: 'Approve execution',
326
366
  pendingReview: 'Pending review',
367
+ noToolToBeUsed: 'No tool will be needed',
327
368
  },
328
369
  pt: {
329
370
  step: 'Passo',
@@ -343,5 +384,6 @@ const dictionary = {
343
384
  viewDetails: 'Ver detalhes',
344
385
  approveTool: 'Aprovar execução',
345
386
  pendingReview: 'Revisão pendente',
387
+ noToolToBeUsed: 'Nenhuma tool será necessária',
346
388
  },
347
389
  } satisfies Dictionary
@@ -292,8 +292,10 @@ export const ChatList: IStyledComponentBase<
292
292
  }
293
293
  }
294
294
 
295
- .step-actions {
296
- margin-top: 8px;
295
+ }
296
+
297
+ .step-actions {
298
+ margin-top: 16px;
297
299
  display: flex;
298
300
  gap: 6px;
299
301
 
@@ -303,7 +305,6 @@ export const ChatList: IStyledComponentBase<
303
305
  align-items: center;
304
306
  }
305
307
  }
306
- }
307
308
 
308
309
  .markdown img {
309
310
  max-width: 70%;