@stack-spot/ai-chat-widget 2.3.0-alpha.1 → 2.3.0

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 (93) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/StackspotAIWidget.d.ts +1 -5
  3. package/dist/StackspotAIWidget.d.ts.map +1 -1
  4. package/dist/StackspotAIWidget.js +2 -2
  5. package/dist/StackspotAIWidget.js.map +1 -1
  6. package/dist/app-metadata.json +5 -5
  7. package/dist/chat-interceptors/send-message.d.ts.map +1 -1
  8. package/dist/chat-interceptors/send-message.js +125 -1
  9. package/dist/chat-interceptors/send-message.js.map +1 -1
  10. package/dist/state/ChatEntry.d.ts.map +1 -1
  11. package/dist/state/ChatState.d.ts +4 -0
  12. package/dist/state/ChatState.d.ts.map +1 -1
  13. package/dist/state/ChatState.js.map +1 -1
  14. package/dist/utils/check-is-trial.d.ts.map +1 -1
  15. package/dist/utils/check-is-trial.js +2 -6
  16. package/dist/utils/check-is-trial.js.map +1 -1
  17. package/dist/utils/planning-tool.d.ts +17 -0
  18. package/dist/utils/planning-tool.d.ts.map +1 -0
  19. package/dist/utils/planning-tool.js +32 -0
  20. package/dist/utils/planning-tool.js.map +1 -0
  21. package/dist/utils/update-tool-step.d.ts +3 -0
  22. package/dist/utils/update-tool-step.d.ts.map +1 -0
  23. package/dist/utils/update-tool-step.js +23 -0
  24. package/dist/utils/update-tool-step.js.map +1 -0
  25. package/dist/views/Agents/AgentsTab.d.ts.map +1 -1
  26. package/dist/views/Agents/AgentsTab.js +3 -4
  27. package/dist/views/Agents/AgentsTab.js.map +1 -1
  28. package/dist/views/Agents/useAgentFavorites.d.ts.map +1 -1
  29. package/dist/views/Agents/useAgentFavorites.js +1 -3
  30. package/dist/views/Agents/useAgentFavorites.js.map +1 -1
  31. package/dist/views/Chat/ChatMessage.d.ts +1 -1
  32. package/dist/views/Chat/ChatMessage.d.ts.map +1 -1
  33. package/dist/views/Chat/ChatMessage.js +21 -6
  34. package/dist/views/Chat/ChatMessage.js.map +1 -1
  35. package/dist/views/Chat/StepsList.d.ts +12 -2
  36. package/dist/views/Chat/StepsList.d.ts.map +1 -1
  37. package/dist/views/Chat/StepsList.js +155 -18
  38. package/dist/views/Chat/StepsList.js.map +1 -1
  39. package/dist/views/Chat/styled.d.ts.map +1 -1
  40. package/dist/views/Chat/styled.js +17 -10
  41. package/dist/views/Chat/styled.js.map +1 -1
  42. package/dist/views/MessageInput/AgentSelector.d.ts.map +1 -1
  43. package/dist/views/MessageInput/AgentSelector.js +2 -8
  44. package/dist/views/MessageInput/AgentSelector.js.map +1 -1
  45. package/dist/views/MessageInput/ButtonAgent.js +1 -1
  46. package/dist/views/MessageInput/ButtonAgent.js.map +1 -1
  47. package/dist/views/MessageInput/ButtonBar.d.ts +1 -2
  48. package/dist/views/MessageInput/ButtonBar.d.ts.map +1 -1
  49. package/dist/views/MessageInput/ButtonBar.js +2 -2
  50. package/dist/views/MessageInput/ButtonBar.js.map +1 -1
  51. package/dist/views/MessageInput/QuickCommandSelector.js +1 -1
  52. package/dist/views/MessageInput/QuickCommandSelector.js.map +1 -1
  53. package/dist/views/MessageInput/dictionary.d.ts +1 -1
  54. package/dist/views/MessageInput/index.d.ts +1 -2
  55. package/dist/views/MessageInput/index.d.ts.map +1 -1
  56. package/dist/views/MessageInput/index.js +2 -2
  57. package/dist/views/MessageInput/index.js.map +1 -1
  58. package/dist/views/Steps/FlowChart/NodeStep.js +1 -1
  59. package/dist/views/Steps/FlowChart/NodeStep.js.map +1 -1
  60. package/dist/views/Steps/FlowChart/layout.d.ts +1 -1
  61. package/dist/views/Steps/FlowChart/layout.d.ts.map +1 -1
  62. package/dist/views/Steps/FlowChart/layout.js +1 -0
  63. package/dist/views/Steps/FlowChart/layout.js.map +1 -1
  64. package/dist/views/Steps/FlowChart/types.d.ts +1 -1
  65. package/dist/views/Steps/FlowChart/types.d.ts.map +1 -1
  66. package/dist/views/Steps/StepModal.js +2 -2
  67. package/dist/views/Steps/StepModal.js.map +1 -1
  68. package/dist/views/Steps/dictionary.d.ts +1 -1
  69. package/dist/views/Steps/utils.d.ts +1 -1
  70. package/dist/views/Steps/utils.d.ts.map +1 -1
  71. package/package.json +3 -3
  72. package/src/StackspotAIWidget.tsx +1 -7
  73. package/src/app-metadata.json +5 -5
  74. package/src/chat-interceptors/send-message.ts +137 -2
  75. package/src/state/ChatEntry.ts +6 -6
  76. package/src/state/ChatState.ts +4 -0
  77. package/src/utils/check-is-trial.ts +2 -5
  78. package/src/utils/planning-tool.ts +41 -0
  79. package/src/utils/update-tool-step.tsx +27 -0
  80. package/src/views/Agents/AgentsTab.tsx +3 -4
  81. package/src/views/Agents/useAgentFavorites.ts +1 -3
  82. package/src/views/Chat/ChatMessage.tsx +25 -5
  83. package/src/views/Chat/StepsList.tsx +337 -44
  84. package/src/views/Chat/styled.ts +17 -10
  85. package/src/views/MessageInput/AgentSelector.tsx +2 -7
  86. package/src/views/MessageInput/ButtonAgent.tsx +1 -1
  87. package/src/views/MessageInput/ButtonBar.tsx +1 -3
  88. package/src/views/MessageInput/QuickCommandSelector.tsx +1 -1
  89. package/src/views/MessageInput/index.tsx +4 -4
  90. package/src/views/Steps/FlowChart/NodeStep.tsx +1 -1
  91. package/src/views/Steps/FlowChart/layout.ts +1 -0
  92. package/src/views/Steps/FlowChart/types.ts +1 -1
  93. package/src/views/Steps/StepModal.tsx +2 -2
@@ -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
  */
@@ -1,9 +1,6 @@
1
1
  import { NetworkClient } from '@stack-spot/portal-network'
2
2
 
3
3
  export const checkIsTrial = () => {
4
- const hasSession = NetworkClient.sessionManager?.hasSession()
5
- if (hasSession){
6
- const trialInfo = NetworkClient.sessionManager?.getSession()?.getTokenData()?.trial_account_status
7
- return !!trialInfo
8
- } return false
4
+ const trialInfo = NetworkClient.sessionManager?.getSession()?.getTokenData()?.trial_account_status
5
+ return !!trialInfo
9
6
  }
@@ -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
+ }
@@ -7,7 +7,7 @@ import { useMemo, useState } from 'react'
7
7
  import { NavigationComponent } from '../../components/ComponentNavigator'
8
8
  import { DescribedRadioGroup } from '../../components/form/DescribedRadioGroup'
9
9
  import { WorkspaceTabNavigator } from '../../components/WorkspaceTabNavigator'
10
- import { useCurrentChat, useCurrentChatState } from '../../context/hooks'
10
+ import { useCurrentChat } from '../../context/hooks'
11
11
  import { useRightPanel } from '../../right-panel/hooks'
12
12
  import { ChatProperties } from '../../state/ChatState'
13
13
  import { AgentDescription } from './AgentDescription'
@@ -28,11 +28,10 @@ export const AgentsTab = ({ visibility, workspaceId, agent, showSubmitButton = t
28
28
  const { useFavorites, onAddFavorite, onRemoveFavorite } = useAgentFavorites()
29
29
  const [submitEnabled, setSubmitEnabled] = useState(false)
30
30
  const listFavorites = useFavorites()
31
- const isAgentEnabled = useCurrentChatState('features').agent
32
- const agentDefault = agentToolsClient.agentDefault.useQuery({ enabled: isAgentEnabled })
31
+ const agentDefault = agentToolsClient.agentDefault.useQuery()
33
32
  const agents = workspaceId
34
33
  ? workspaceAiClient.getAgentFromWorkspaceAi.useQuery({ workspaceId }) as AgentResponseWithBuiltIn[]
35
- : agentToolsClient.agents.useQuery({ visibility }, { enabled: isAgentEnabled })
34
+ : agentToolsClient.agents.useQuery({ visibility })
36
35
 
37
36
  const initialValue = useMemo<AgentResponseWithBuiltIn | undefined>(
38
37
  () => {
@@ -1,10 +1,8 @@
1
1
  /* eslint-disable filenames/match-regex */
2
2
  import { agentToolsClient } from '@stack-spot/portal-network'
3
- import { useCurrentChatState } from '../../context/hooks'
4
3
 
5
4
  export function useAgentFavorites() {
6
- const isAgentEnabled = useCurrentChatState('features').agent
7
- const useFavorites = () => agentToolsClient.agents.useQuery({ visibility: 'favorite' }, { enabled: isAgentEnabled })
5
+ const useFavorites = () => agentToolsClient.agents.useQuery({ visibility: 'favorite' })
8
6
  const [addFavorite, pendingAddFav] = agentToolsClient.addFavorite.useMutation()
9
7
  const [removeFavorite, pendingRemoveFav] = agentToolsClient.removeFavorite.useMutation()
10
8
 
@@ -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">
@@ -1,96 +1,389 @@
1
- import { Icon } from '@stack-spot/citric-icons'
2
- import { Button, ProgressCircular, Text } from '@stack-spot/citric-react'
1
+ import { Accordion, Badge, Button, Card, Column, Divider, Icon, IconBox, ImageWithFallback, ProgressCircular, Row, Skeleton, Text } from '@stack-spot/citric-react'
3
2
  import { AnimatedHeight } from '@stack-spot/portal-components/AnimatedHeight'
4
- import { ChatStep, StepChatStep } from '@stack-spot/portal-network'
3
+ import { ChatStep, StepChatStep, ToolChatStep } from '@stack-spot/portal-network'
5
4
  import { theme } from '@stack-spot/portal-theme'
6
5
  import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
7
- import { findLastIndex } from 'lodash'
8
- import { useState } from 'react'
9
- import { useWidget } from '../../context/hooks'
6
+ import { findLast, findLastIndex } from 'lodash'
7
+ import React, { useEffect, useMemo } from 'react'
8
+ import styled from 'styled-components'
9
+ import { Markdown } from '../../components/Markdown'
10
+ import { useChat, useChatMessages, useCurrentChat, useCurrentChatMessages, useWidget } from '../../context/hooks'
11
+ import { ChatEntry } from '../../state/ChatEntry'
12
+ import { planningToolDictionaryHelper } from '../../utils/planning-tool'
13
+ import { updateToolStep } from '../../utils/update-tool-step'
14
+ import { onCopyCode } from './events'
10
15
 
11
16
  interface Props {
12
17
  steps: ChatStep[],
13
18
  messageId: number,
14
19
  chatId: string,
20
+ userHasAlreadyAnswered?: boolean,
21
+ }
22
+
23
+ interface StepChatStepWithTarget extends Omit<StepChatStep, 'status' | 'id' | 'type'> {
24
+ status: 'pending' | 'running' | 'success' | 'error' | 'target' | 'awaiting_approval',
15
25
  }
16
26
 
17
27
  interface StepProps {
18
- step: StepChatStep,
28
+ step: StepChatStepWithTarget,
19
29
  index: number,
20
- total: number,
30
+ total?: number,
31
+ totalTools?: number,
32
+ isAllDone?: boolean,
21
33
  onClick?: () => void,
22
34
  }
23
35
 
24
- function getStatusIcon(status: ChatStep['status']) {
36
+ function getStatusIcon(status: StepChatStepWithTarget['status'] | 'target', isDone?: boolean) {
25
37
  const iconProps = { style: { color: theme.color.light[700] }, size: 'xs' } as const
26
38
  switch (status) {
27
39
  case 'error': return <Icon group="fill" icon="TimesCircle" {...iconProps} />
28
40
  case 'success': return <Icon group="fill" icon="CheckCircle" {...iconProps} />
29
- case 'pending': return <Icon icon="Spaces" {...iconProps} />
41
+ case 'pending': return <Icon group="fill" icon="Circle" {...iconProps} style={{ color: theme.color.light[600] }} />
42
+ case 'awaiting_approval': return <Icon group="fill" icon="ExclamationTriangle" {...iconProps} />
43
+ case 'target': return <Icon icon="Target" {...iconProps} style={{ color: isDone ? theme.color.light[700] : theme.color.light[600] }} />
30
44
  case 'running': return <ProgressCircular className="loading" colorScheme="inverse" size="xs" />
31
45
  }
32
46
  }
33
47
 
34
- const Step = ({ step, index, total, onClick }: StepProps) => {
48
+ const StepAccordionHeader = ({ step, index, expand }: Pick<StepProps, 'step' | 'index'> & { expand: React.ReactElement }) => {
49
+ const t = useTranslate(dictionary)
50
+ return <Row gap="8px">
51
+ {expand}
52
+ {step.status === 'target' ? <Text className="step-title" appearance="body2" color="light.700">{t.planGoal}:</Text> :
53
+ <Badge colorScheme="inverse" appearance="square">
54
+ {t.step} {index}
55
+ </Badge>}
56
+ <Text className="step-title" appearance="body2">
57
+ {step.input}
58
+ </Text>
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>}
64
+ </Row>
65
+ }
66
+
67
+ const StyledCard = styled(Card)`
68
+ &:hover {
69
+ background-color: ${theme.color.light[500]}
70
+ }
71
+ `
72
+
73
+ const Step = ({ step, index, onClick, total, totalTools, isAllDone }: StepProps) => {
35
74
  const t = useTranslate(dictionary)
75
+ const status = getStatusIcon(step.status, isAllDone)
76
+ const hasTools = step.attempts?.[0]?.tools && step.attempts?.[0]?.tools?.length > 0
77
+
36
78
  return (
37
79
  <li tabIndex={onClick ? 0 : undefined} onClick={onClick} role={onClick ? 'button' : 'listitem'}>
38
- <div className="step-status-icon">{getStatusIcon(step.status)}</div>
39
- <Text className="step-title" appearance="microtext1" color="light.700">
40
- {t.step} {index}/{total}: {step.input}
41
- </Text>
80
+ <Row gap="4px" alignItems="center">
81
+ <div className="step-status-icon">{status}</div>
82
+ <StyledCard p="8px" w="80%">
83
+ <Accordion header={expand => <StepAccordionHeader step={step} index={index} expand={expand} />}>
84
+ <Column pt="12px">
85
+ {total ?
86
+ <Row gap="40px">
87
+ <Row gap="4px">
88
+ <Icon icon="Hashtag" size="sm" color="light.700" />
89
+ <Text color="light.700">{t.totalSteps}</Text>
90
+ {total}
91
+ </Row>
92
+ <Row gap="4px">
93
+ <Icon icon="BorderRadius" size="sm" color="light.700" />
94
+ <Text color="light.700">{t.totalTools}</Text>
95
+ {totalTools ?? 0}
96
+ </Row>
97
+ </Row>
98
+ : <>
99
+ <Row pb="8px">
100
+ <Icon icon="Target" size="sm" color="light.700" />
101
+ <Text color="light.700" tag="span" style={{ margin: '0 4px' }}>{t.stepGoal}:</Text>
102
+ <Text tag="span">
103
+ {step.input}
104
+ </Text>
105
+ </Row>
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">
117
+ <Icon icon="BorderRadius" size="sm" color="light.700" />
118
+ <Text color="light.700">{t.noToolToBeUsed}</Text>
119
+ </Row>
120
+ }
121
+ </>
122
+ }
123
+ </Column>
124
+ </Accordion>
125
+ </StyledCard>
126
+ </Row>
42
127
  </li>
43
128
  )
44
129
  }
45
130
 
46
- export const StepsList = ({ steps, chatId, messageId }: Props) => {
131
+ export const StepsPlaceholder = () => {
132
+ const t = useTranslate(dictionary)
133
+ return <Card gap="8px">
134
+ <Row gap="8px">
135
+ <ProgressCircular colorScheme="inverse" size="xs" />
136
+ <Text color="light.700">{t.generatingPlan}</Text>
137
+ </Row>
138
+ <Text color="light.700">
139
+ {t.analyzingRequirements}
140
+ </Text>
141
+ <Row gap="12px">
142
+ <Skeleton height="31px" width="148px" bgLevel={600} />
143
+ <Skeleton height="31px" width="148px" bgLevel={600} />
144
+ <Skeleton height="31px" width="148px" bgLevel={600} />
145
+ <Skeleton height="31px" width="148px" bgLevel={600} />
146
+ </Row>
147
+ </Card>
148
+ }
149
+
150
+ const AwaitingApproval = ({ customApproveText, chatId }: { chatId: string, customApproveText?: string }) => {
151
+ const t = useTranslate(dictionary)
152
+ const chat = useChat(chatId)
153
+
154
+ const onAnswer = (response: string) => {
155
+ chat.pushMessage(ChatEntry.createUserEntry('', false, undefined, undefined, response))
156
+ }
157
+
158
+ return <>
159
+ <Row gap="8px">
160
+ <Button colorScheme="light" onClick={() => onAnswer(t.cancel)}>
161
+ <Row gap="8px">
162
+ <Icon icon="Stop" />
163
+ {t.cancel}
164
+ </Row>
165
+ </Button>
166
+
167
+ <Button colorScheme="inverse" onClick={() => onAnswer(customApproveText ?? t.approve)}>
168
+ <Row gap="8px">
169
+ <Icon group="fill" icon="Play" />
170
+ {customApproveText ?? t.approve}
171
+ </Row>
172
+ </Button>
173
+ </Row>
174
+ </>
175
+ }
176
+
177
+ export const ToolStepsList = ({ toolStep, messageId, chatId }: { toolStep: ToolChatStep, messageId: number, chatId: string }) => {
178
+ const t = useTranslate(dictionary)
179
+ const chat = useCurrentChat()
180
+ const messages = useCurrentChatMessages()
181
+ const inputParsed = `\`\`\`json
182
+ ${JSON.stringify(toolStep?.input, null, 2)}
183
+ \`\`\``
184
+
185
+ const tool = useMemo(() => {
186
+ if (!toolStep) return undefined
187
+ return toolStep.attempts?.[0].tools?.[0]
188
+ }, [toolStep])
189
+
190
+ useEffect(() => {
191
+ if (!toolStep) return undefined
192
+ const executionId = toolStep.attempts?.[0].tools?.[0].executionId
193
+ if (!executionId) return
194
+
195
+ updateToolStep(messages, executionId, toolStep.status)
196
+
197
+ }, [messages, toolStep, toolStep.status])
198
+
199
+ return <>
200
+ {toolStep && tool ? <AnimatedHeight>
201
+ <div className="steps">
202
+ <Badge colorPalette="yellow" appearance="square">
203
+ <Icon icon="StopWatch" />
204
+ <Text>{tool.name} {t.keepWorking}</Text>
205
+ </Badge>
206
+ <Card mt="16px" gap="8px" bgLevel={500}>
207
+ <Row>
208
+ <ImageWithFallback src={tool.image} width="32px" fallback={<IconBox appearance="circle" icon="StackSpot" />} />
209
+ <Text>{tool.name}</Text>
210
+ </Row>
211
+
212
+ <Text>
213
+ {toolStep.user_question}
214
+ </Text>
215
+
216
+ <Accordion header={expand => <Row gap="8px" mb="4px">
217
+ <Card p="4px" bgLevel={400}>{expand}</Card>
218
+ <Text > {t.viewDetails} </Text>
219
+ </Row>}>
220
+ <Markdown onCopyCode={(code) => onCopyCode(code, `${messageId}`, chat)}>
221
+ {inputParsed}
222
+ </Markdown>
223
+ </Accordion>
224
+
225
+ <AwaitingApproval customApproveText={t.approveTool} chatId={chatId} />
226
+ </Card>
227
+ </div>
228
+ </AnimatedHeight> : null}
229
+ </>
230
+ }
231
+
232
+ export const StepsList = ({ steps, messageId, chatId, userHasAlreadyAnswered }: Props) => {
47
233
  const t = useTranslate(dictionary)
48
- const [isExpanded, setExpanded] = useState(false)
49
- 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
+
250
+ const planning = steps.filter(s => s.type === 'planning')
251
+
252
+ useEffect(() => {
253
+ actualSteps.map((item) => {
254
+ const executionId = item.attempts[0]?.tools?.[0].executionId
255
+ if (executionId) {
256
+ planningToolDictionaryHelper.setMessageIdPlanningStepToolExecutionId(`${messageId}`, executionId)
257
+ }
258
+ })
259
+ }, [actualSteps, messageId])
260
+
261
+ const toolsStep = findLast(steps, s => s.type === 'tool')
262
+
263
+ useEffect(() => {
264
+ const executionId = toolsStep?.attempts?.[0]?.tools?.[0]?.executionId
265
+ if (executionId) {
266
+ planningToolDictionaryHelper.setMessageIdToolStepToolExecutionId(`${messageId}`, executionId)
267
+ }
268
+ }, [toolsStep, messageId])
269
+
270
+ const planningGoal = planning?.[0]?.goal
271
+ const isLastStepDone = actualSteps[actualSteps.length - 1]?.status !== 'running' &&
272
+ actualSteps[actualSteps.length - 1]?.status !== 'pending'
273
+ const totalTools = useMemo(() => actualSteps?.reduce((sum, step) => {
274
+ const firstAttempt = step.attempts && step.attempts[0]
275
+ const toolsCount = firstAttempt.tools?.length ?? 0
276
+ return sum + toolsCount
277
+ }, 0), [steps])
278
+
50
279
  let currentStepIndex = findLastIndex(actualSteps, s => s.status === 'running' || s.status === 'success')
51
280
  if (currentStepIndex === -1) currentStepIndex = 0
52
- const widget = useWidget()
53
281
 
54
- function openToolsPanel() {
55
- widget.set('currentMessageInPanel', { chatId, messageId })
56
- widget.set('panel', 'steps')
57
- }
58
-
59
- return (
60
- <AnimatedHeight>
282
+ return (<>
283
+ {actualSteps.length > 0 ? <AnimatedHeight>
61
284
  <div className="steps">
62
- <ul>
63
- {isExpanded
64
- ? actualSteps.map((s, i) => <Step step={s} key={i} index={i + 1} total={actualSteps.length} />)
65
- : <Step
66
- step={actualSteps[currentStepIndex]}
67
- index={currentStepIndex + 1}
68
- total={actualSteps.length}
69
- onClick={() => setExpanded(true)}
70
- />
71
- }
285
+ <Row gap="14px" mb="8px" ml="5px">
286
+ <Icon icon="Target" size="sm" color="light.600" />
287
+ <Text>{t.executionPlan}</Text>
288
+ </Row>
289
+
290
+ <ul className="steps-list">
291
+ {actualSteps.map((s, i) => <Step step={s} key={i} index={i + 1} />)}
292
+
293
+ <Step
294
+ step={{ status: 'target', input: planningGoal, attempts: [] }}
295
+ index={currentStepIndex + 1}
296
+ total={actualSteps.length}
297
+ totalTools={totalTools}
298
+ isAllDone={isLastStepDone}
299
+ />
72
300
  </ul>
73
- {isExpanded && <div className="step-actions">
74
- <Button colorScheme="light" size="sm" onClick={() => setExpanded(false)}>{t.hideSteps}</Button>
75
- <Button colorScheme="light" size="sm" className="icon-button details" onClick={openToolsPanel}>
76
- <Icon group="fill" icon="Play" size="xs" />
77
- {t.detailed}
78
- </Button>
79
- </div>}
301
+
302
+ <Column gap="8px" mt="8px">
303
+ <Divider colorScheme="light" />
304
+ <Text color="light.700">{planning?.[0]?.user_question}</Text>
305
+ {!userHasAlreadyAnswered && planning?.[0]?.status === 'awaiting_approval' && <AwaitingApproval chatId={chatId} />}
306
+ </Column>
307
+
80
308
  </div>
81
- </AnimatedHeight>
309
+ </AnimatedHeight> : null}
310
+
311
+ {toolsStep && toolsStep.status === 'awaiting_approval' && !userHasAlreadyAnswered &&
312
+ <ToolStepsList toolStep={toolsStep} messageId={messageId} chatId={chatId} />}
313
+ </>
82
314
  )
83
315
  }
84
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
+
85
348
  const dictionary = {
86
349
  en: {
87
350
  step: 'Step',
88
351
  hideSteps: 'Hide steps',
89
352
  detailed: 'View detailed mode',
353
+ generatingPlan: 'Generating execution plan...',
354
+ analyzingRequirements: 'Analyzing task requirements',
355
+ executionPlan: 'Execution Plan',
356
+ planGoal: 'Plan Goal',
357
+ toolsToBeExecuted: 'Tools to be executed',
358
+ totalSteps: 'Total Steps',
359
+ totalTools: 'Total Tools',
360
+ stepGoal: 'Step Goal',
361
+ cancel: 'Cancel execution',
362
+ approve: 'Approve & Execute Plan',
363
+ keepWorking: 'will keep working after your answer',
364
+ viewDetails: 'View details',
365
+ approveTool: 'Approve execution',
366
+ pendingReview: 'Pending review',
367
+ noToolToBeUsed: 'No tool will be needed',
90
368
  },
91
369
  pt: {
92
370
  step: 'Passo',
93
371
  hideSteps: 'Esconder passos',
94
372
  detailed: 'Ver modo detalhado',
373
+ generatingPlan: 'Gerando plano de execução...',
374
+ analyzingRequirements: 'Analisando os requisitos da task',
375
+ executionPlan: 'Plano de Execução',
376
+ planGoal: 'Finalidade do Plano',
377
+ toolsToBeExecuted: 'Tools a serem executadas',
378
+ totalSteps: 'Total de Passos',
379
+ totalTools: 'Total de Tools',
380
+ stepGoal: 'Objetivo do passo',
381
+ cancel: 'Cancelar execução',
382
+ approve: 'Aprovar & Executar plano',
383
+ keepWorking: 'continuará trabalhando após a sua resposta',
384
+ viewDetails: 'Ver detalhes',
385
+ approveTool: 'Aprovar execução',
386
+ pendingReview: 'Revisão pendente',
387
+ noToolToBeUsed: 'Nenhuma tool será necessária',
95
388
  },
96
389
  } satisfies Dictionary