@stack-spot/ai-chat-widget 3.4.1-beta.5 → 3.6.0-beta.5

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 (81) hide show
  1. package/CHANGELOG.md +89 -6
  2. package/dist/app-metadata.json +4 -4
  3. package/dist/chat-interceptors/quick-commands.js +2 -2
  4. package/dist/chat-interceptors/quick-commands.js.map +1 -1
  5. package/dist/chat-interceptors/send-message.d.ts.map +1 -1
  6. package/dist/chat-interceptors/send-message.js +8 -17
  7. package/dist/chat-interceptors/send-message.js.map +1 -1
  8. package/dist/components/form/DescribedCheckboxGroup.d.ts +3 -1
  9. package/dist/components/form/DescribedCheckboxGroup.d.ts.map +1 -1
  10. package/dist/components/form/DescribedCheckboxGroup.js +31 -19
  11. package/dist/components/form/DescribedCheckboxGroup.js.map +1 -1
  12. package/dist/context/hooks.d.ts +1 -0
  13. package/dist/context/hooks.d.ts.map +1 -1
  14. package/dist/context/hooks.js +24 -0
  15. package/dist/context/hooks.js.map +1 -1
  16. package/dist/state/ChatEntry.d.ts +8 -0
  17. package/dist/state/ChatEntry.d.ts.map +1 -1
  18. package/dist/state/ChatEntry.js +1 -1
  19. package/dist/state/ChatEntry.js.map +1 -1
  20. package/dist/state/ChatState.d.ts +6 -0
  21. package/dist/state/ChatState.d.ts.map +1 -1
  22. package/dist/state/ChatState.js +15 -0
  23. package/dist/state/ChatState.js.map +1 -1
  24. package/dist/utils/tools.d.ts +17 -8
  25. package/dist/utils/tools.d.ts.map +1 -1
  26. package/dist/utils/tools.js +20 -9
  27. package/dist/utils/tools.js.map +1 -1
  28. package/dist/views/Agents/AgentDescription.d.ts.map +1 -1
  29. package/dist/views/Agents/AgentDescription.js +5 -14
  30. package/dist/views/Agents/AgentDescription.js.map +1 -1
  31. package/dist/views/Agents/AgentsTab.d.ts.map +1 -1
  32. package/dist/views/Agents/AgentsTab.js +3 -2
  33. package/dist/views/Agents/AgentsTab.js.map +1 -1
  34. package/dist/views/Chat/ChatMessage.d.ts +1 -1
  35. package/dist/views/Chat/ChatMessage.d.ts.map +1 -1
  36. package/dist/views/Chat/ChatMessage.js +19 -20
  37. package/dist/views/Chat/ChatMessage.js.map +1 -1
  38. package/dist/views/Chat/StepsList.d.ts +2 -8
  39. package/dist/views/Chat/StepsList.d.ts.map +1 -1
  40. package/dist/views/Chat/StepsList.js +8 -7
  41. package/dist/views/Chat/StepsList.js.map +1 -1
  42. package/dist/views/ChatHistory/utils.d.ts.map +1 -1
  43. package/dist/views/ChatHistory/utils.js +94 -2
  44. package/dist/views/ChatHistory/utils.js.map +1 -1
  45. package/dist/views/KnowledgeSources.d.ts +1 -1
  46. package/dist/views/KnowledgeSources.d.ts.map +1 -1
  47. package/dist/views/KnowledgeSources.js +31 -45
  48. package/dist/views/KnowledgeSources.js.map +1 -1
  49. package/dist/views/MessageInput/QuickCommandSelector.d.ts.map +1 -1
  50. package/dist/views/MessageInput/QuickCommandSelector.js +29 -52
  51. package/dist/views/MessageInput/QuickCommandSelector.js.map +1 -1
  52. package/dist/views/MessageInput/index.d.ts.map +1 -1
  53. package/dist/views/MessageInput/index.js +9 -1
  54. package/dist/views/MessageInput/index.js.map +1 -1
  55. package/dist/views/MessageInput/styled.d.ts.map +1 -1
  56. package/dist/views/MessageInput/styled.js +4 -0
  57. package/dist/views/MessageInput/styled.js.map +1 -1
  58. package/dist/views/Resources.js +8 -5
  59. package/dist/views/Resources.js.map +1 -1
  60. package/dist/views/Tools.js +1 -1
  61. package/dist/views/Tools.js.map +1 -1
  62. package/package.json +3 -3
  63. package/src/app-metadata.json +4 -4
  64. package/src/chat-interceptors/quick-commands.ts +2 -2
  65. package/src/chat-interceptors/send-message.ts +8 -19
  66. package/src/components/form/DescribedCheckboxGroup.tsx +61 -35
  67. package/src/context/hooks.ts +24 -0
  68. package/src/state/ChatEntry.ts +9 -1
  69. package/src/state/ChatState.ts +16 -0
  70. package/src/utils/tools.ts +28 -17
  71. package/src/views/Agents/AgentDescription.tsx +40 -36
  72. package/src/views/Agents/AgentsTab.tsx +8 -7
  73. package/src/views/Chat/ChatMessage.tsx +23 -23
  74. package/src/views/Chat/StepsList.tsx +9 -9
  75. package/src/views/ChatHistory/utils.ts +96 -3
  76. package/src/views/KnowledgeSources.tsx +57 -77
  77. package/src/views/MessageInput/QuickCommandSelector.tsx +39 -93
  78. package/src/views/MessageInput/index.tsx +10 -0
  79. package/src/views/MessageInput/styled.ts +4 -0
  80. package/src/views/Resources.tsx +11 -10
  81. package/src/views/Tools.tsx +1 -1
@@ -1,15 +1,13 @@
1
1
  import { Icon } from '@stack-spot/citric-icons'
2
- import { Badge, Card, IconBox, ImageBox, ImageWithFallback, Skeleton, Text } from '@stack-spot/citric-react'
2
+ import { AsyncContent, Badge, Card, IconBox, ImageBox, ImageWithFallback, Text } from '@stack-spot/citric-react'
3
3
  import { agentToolsClient } from '@stack-spot/portal-network'
4
4
  import { useMemo } from 'react'
5
- import { toolById } from '../../utils/tools'
6
5
  import { useAgentsDictionary } from './dictionary'
7
6
  import { AgentDescriptionBox } from './styled'
8
7
 
9
8
  export const AgentDescription = ({ agentId }: { agentId?: string }) => {
10
9
  const t = useAgentsDictionary()
11
- const [agent, , , { isLoading }] = agentToolsClient.agent.useStatefulQuery({ agentId: agentId! }, { enabled: !!agentId })
12
- const [toolKits, , , { isLoading: isLoadingToolKit }] = agentToolsClient.tools.useStatefulQuery({})
10
+ const [agent,, error, { isLoading }] = agentToolsClient.agent.useStatefulQuery({ agentId: agentId! }, { enabled: !!agentId })
13
11
  const numberOfKnowledgeSources = agent?.knowledge_sources_config?.knowledge_sources.length ?? 0
14
12
 
15
13
  const knowledgeSources = useMemo(
@@ -23,13 +21,6 @@ export const AgentDescription = ({ agentId }: { agentId?: string }) => {
23
21
  )),
24
22
  [agent],
25
23
  )
26
- const skeleton = useMemo(() => {
27
- const loadingKS: React.ReactElement[] = []
28
- for (let i = 0; i < numberOfKnowledgeSources; i++) {
29
- loadingKS.push(<li key={i}><Badge colorPalette="teal" appearance="square"><Skeleton className="ks-skeleton" /></Badge></li>)
30
- }
31
- return loadingKS
32
- }, [numberOfKnowledgeSources])
33
24
 
34
25
  const { tools, multiAgents } = useMemo(() => {
35
26
  const tools: React.ReactElement[] = []
@@ -39,12 +30,11 @@ export const AgentDescription = ({ agentId }: { agentId?: string }) => {
39
30
  for (const toolkit of builtInTools) {
40
31
  for (const tool of toolkit.tools ?? []) {
41
32
  if (toolkit.id == 'UTILITIES'){
42
- const toolWithImage = toolById(tool.id, toolKits)
43
33
  tools.push(
44
34
  <li key={tool.id}>
45
35
  <Card gap="10px" direction="row" flex={1} size="xxs" bgLevel={500}>
46
- <ImageBox><ImageWithFallback src={toolWithImage?.image} fallback={<Icon icon="Cog" />} /></ImageBox>
47
- <Text color="light.contrastText">{toolWithImage?.name || toolWithImage?.id || 'unknown'}</Text>
36
+ <ImageBox><ImageWithFallback src={toolkit.image_url} fallback={<Icon icon="Cog" />} /></ImageBox>
37
+ <Text color="light.contrastText">{tool?.name || tool?.id || 'unknown'}</Text>
48
38
  </Card>
49
39
  </li>,
50
40
  )
@@ -76,28 +66,42 @@ export const AgentDescription = ({ agentId }: { agentId?: string }) => {
76
66
  return { tools, multiAgents }
77
67
  }, [agent])
78
68
 
69
+ const mcpToolkits = useMemo(() => agent?.toolkits?.mcp_toolkits?.map(t => (
70
+ <li key={`mcp-${t.id}`}>
71
+ <Card gap="10px" direction="row" flex={1} size="xxs" bgLevel={500}>
72
+ <ImageBox><ImageWithFallback src={t.avatar ?? undefined} fallback={<Icon icon="Cog" />} /></ImageBox>
73
+ <Text color="light.contrastText">{t.name}</Text>
74
+ </Card>
75
+ </li>
76
+ )), [agent?.toolkits?.mcp_toolkits])
77
+
79
78
  return (
80
- <AgentDescriptionBox>
81
- {agent?.description && <section>
82
- <Text appearance="microtext1" className="title">{t.description}</Text>
83
- <Text>{agent?.description}</Text>
84
- </section>}
85
- {(!!numberOfKnowledgeSources || !!knowledgeSources?.length) && <section>
86
- <Text appearance="microtext1" className="title">Knowledge sources</Text>
87
- <ul>{isLoading || isLoadingToolKit ? skeleton : knowledgeSources}</ul>
88
- </section>}
89
- {!!tools?.length && <section>
90
- <Text appearance="microtext1" className="title">{t.tools}</Text>
91
- <ul>{tools}</ul>
92
- </section>}
93
- {!!multiAgents?.length && <section>
94
- <Text appearance="microtext1" className="title">{t.multiAgent}</Text>
95
- <ul>{multiAgents}</ul>
96
- </section>}
97
- {agent?.model_name && <section>
98
- <Text appearance="microtext1" className="title">LLM</Text>
99
- <Badge colorPalette="orange" appearance="square">{agent?.model_name}</Badge>
100
- </section>}
101
- </AgentDescriptionBox>
79
+ <AsyncContent loading={isLoading} error={error}>
80
+ <AgentDescriptionBox>
81
+ {agent?.description && <section>
82
+ <Text appearance="microtext1" className="title">{t.description}</Text>
83
+ <Text>{agent?.description}</Text>
84
+ </section>}
85
+ {(!!numberOfKnowledgeSources || !!knowledgeSources?.length) && <section>
86
+ <Text appearance="microtext1" className="title">Knowledge sources</Text>
87
+ <ul>{knowledgeSources}</ul>
88
+ </section>}
89
+ {!!tools?.length && <section>
90
+ <Text appearance="microtext1" className="title">{t.tools}</Text>
91
+ <ul>
92
+ {tools}
93
+ {mcpToolkits}
94
+ </ul>
95
+ </section>}
96
+ {!!multiAgents?.length && <section>
97
+ <Text appearance="microtext1" className="title">{t.multiAgent}</Text>
98
+ <ul>{multiAgents}</ul>
99
+ </section>}
100
+ {agent?.model_name && <section>
101
+ <Text appearance="microtext1" className="title">LLM</Text>
102
+ <Badge colorPalette="orange" appearance="square">{agent?.model_name}</Badge>
103
+ </section>}
104
+ </AgentDescriptionBox>
105
+ </AsyncContent>
102
106
  )
103
107
  }
@@ -1,5 +1,5 @@
1
1
  import { Icon } from '@stack-spot/citric-icons'
2
- import { Button } from '@stack-spot/citric-react'
2
+ import { Button, ImageWithFallback } from '@stack-spot/citric-react'
3
3
  import { Placeholder } from '@stack-spot/portal-components/Placeholder'
4
4
  import { AgentResponseWithBuiltIn, agentToolsClient, AgentVisibilityLevel, workspaceAiClient } from '@stack-spot/portal-network'
5
5
  import { WorkspaceResponse } from '@stack-spot/portal-network/api/workspace-ai'
@@ -30,7 +30,7 @@ export const AgentsTab = ({ visibility, workspaceId, agent, showSubmitButton = t
30
30
  const [submitEnabled, setSubmitEnabled] = useState(false)
31
31
  const listFavorites = useFavorites()
32
32
  const agentDefault = agentToolsClient.agentDefault.useQuery()
33
-
33
+
34
34
  const [filter, setFilter] = useState<string | undefined>()
35
35
  const [apiFilter, setApiFilter] = useState<string | undefined>()
36
36
  const [isPending, startTransition] = useTransition()
@@ -47,9 +47,9 @@ export const AgentsTab = ({ visibility, workspaceId, agent, showSubmitButton = t
47
47
  const data = workspaceAiClient.getAgentFromWorkspaceAi.useStatefulQuery({ workspaceId: workspaceId! },
48
48
  { enabled: !!workspaceId })
49
49
  const workspaceAgents = data?.[0] as AgentResponseWithBuiltIn[]
50
-
51
- const [agentsData=[], { fetchNextPage, hasNextPage }] = agentToolsClient.agentsMultipleFilters.useInfiniteQuery({
52
- filters: {
50
+
51
+ const [agentsData = [], { fetchNextPage, hasNextPage }] = agentToolsClient.agentsMultipleFilters.useInfiniteQuery({
52
+ filters: {
53
53
  page: 1,
54
54
  size: 20,
55
55
  name: apiFilter,
@@ -78,7 +78,7 @@ export const AgentsTab = ({ visibility, workspaceId, agent, showSubmitButton = t
78
78
  return <>
79
79
  <div className="content">
80
80
  <DescribedRadioGroup
81
- fetchNextPage={fetchNextPage}
81
+ fetchNextPage={fetchNextPage}
82
82
  hasNextPage={hasNextPage && !workspaceId}
83
83
  options={agents}
84
84
  initialValue={initialValue}
@@ -96,7 +96,8 @@ export const AgentsTab = ({ visibility, workspaceId, agent, showSubmitButton = t
96
96
  isLoading={isPending}
97
97
  data={ag => ({
98
98
  idOrSlug: ag.id,
99
- image: ag.avatar ? <img src={ag.avatar} /> : <Icon icon="Agent" />,
99
+ // below, "key" is important. I don't know why exactly, but sometimes the avatar doesn't update if we don't do this.
100
+ image: <ImageWithFallback key={ag.id} src={ag.avatar ?? undefined} fallback={<Icon icon="Agent" />} />,
100
101
  description: <AgentDescription agentId={ag.id} />,
101
102
  name: ag.name,
102
103
  listFavorites,
@@ -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, useChatMessages, useCurrentChat, useCurrentChatState, useWidget } from '../../context/hooks'
12
+ import { useChatEntry, 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'
@@ -213,26 +213,24 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
213
213
  const widget = useWidget()
214
214
  const chat = useCurrentChat()
215
215
  const agentId = entry.agent?.id ?? ''
216
- const [toolKits] = agentToolsClient.tools.useStatefulQuery({}, { enabled: !!agentId })
216
+ // enabled: we don't want to make any request if there is no agent
217
+ const [agent] = agentToolsClient.agent.useStatefulQuery({ agentId }, { enabled: !!agentId })
218
+ const toolkits = useMemo(() => [
219
+ ...agent?.toolkits?.builtin_toolkits ?? [],
220
+ ...agent?.toolkits?.custom_toolkits ?? [],
221
+ ...agent?.toolkits?.mcp_toolkits ?? [],
222
+ ], [agent])
217
223
  const [agentsTools] = agentToolsClient.agentsByIds.useStatefulQuery(
218
- { searchAgentsRequest: { ids: entry.tools || [''] } }, { enabled: !!entry.tools })
224
+ { searchAgentsRequest: { ids: entry.tools || [''] } }, { enabled: !!entry.tools?.length })
219
225
  const [copied, setCopied] = useState(false)
220
226
  const [showUserButtonCopy, setShowUserButtonCopy] = useState(false)
221
227
  const isPlanning = useCurrentChatState('isPlaning') ?? false
222
228
 
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
-
229
+ // Dynamic tool steps are identified by the "dynamic" id.
230
+ // We're temporarily hiding the toolbox for these dynamic tools while we finalize their UI.
231
+ const shouldHideToolbox = entry?.steps?.some((step) => step?.id === 'dynamic')
232
+ const showToolBox = (!!agentsTools?.length || !!entry.tools?.length) && !shouldHideToolbox
233
+
236
234
  useChatScrollToBottomEffect(ref, [entry])
237
235
  useMidnightUpdateView()
238
236
 
@@ -357,8 +355,10 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
357
355
  widget.set('panel', 'resources')
358
356
  }
359
357
 
360
- return (entry.content || entry.error || !!entry.steps?.length ||
361
- entry.upload?.length) && (!isMessageHidden || !toolsStep || isPlanning) && (
358
+ const shouldShowToolsOnlyMessage = (entry.done !== false || entry.hasPlanning) && !!entry.steps?.length
359
+ const shouldRender = entry.content || entry.error || shouldShowToolsOnlyMessage || !!entry.upload?.length
360
+
361
+ return shouldRender && (
362
362
  <li key={entry.messageId} className={entry.agentType} ref={ref}>
363
363
  <div className="chat-message-container"
364
364
  onMouseEnter={entry.agentType === 'user' ? () => setShowUserButtonCopy(true) : undefined}
@@ -372,8 +372,7 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
372
372
  {entry.badges.map((b, index) => <Badge key={index} colorPalette={b.color ?? 'cyan'} appearance="square">{b.label}</Badge>)}
373
373
  </div>}
374
374
 
375
- {!!entry.steps?.length && <StepsList steps={entry.steps} chatId={chat.id} messageId={message.id}
376
- userHasAlreadyAnswered={userHasAlreadyAnswered} />}
375
+ {!!entry.steps?.length && <StepsList steps={entry.steps} chatId={chat.id} messageId={message.id} />}
377
376
 
378
377
  {renderContent()}
379
378
 
@@ -394,7 +393,7 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
394
393
  ))}</ul>
395
394
  </div>}
396
395
 
397
- {(!!agentsTools?.length || !!entry.tools?.length) &&
396
+ {showToolBox &&
398
397
  <div className="tools-box">
399
398
  <Button appearance="none" onClick={openResourcesPanel} aria-label={t.openResourcesPanel}>
400
399
  {agentsTools?.map((agent) => (
@@ -409,13 +408,14 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
409
408
  </ImageBox>
410
409
  ))}
411
410
  {entry?.tools?.map((id) => {
411
+ // In the multi-agents feature, an agent is returned as a tool, but we don't want to show agents here.
412
412
  if (agentsTools?.some((agent) => agent.id === id)) return null
413
- const tool = toolById(id, toolKits)
413
+ const tool = toolById(id, toolkits)
414
414
  return (
415
415
  <ImageBox key={id} className="agent-info-avatar-resource">
416
416
  <ImageWithFallback
417
417
  src={tool?.image}
418
- fallback={<Icon icon="Cog" />}
418
+ fallback={<Icon icon="Cog" title={tool?.name} aria-label={tool?.name} />}
419
419
  alt={tool?.name}
420
420
  aria-label={tool?.name}
421
421
  title={tool?.name}
@@ -7,7 +7,7 @@ import { findLast, findLastIndex } from 'lodash'
7
7
  import React, { useEffect, useMemo } from 'react'
8
8
  import styled from 'styled-components'
9
9
  import { Markdown } from '../../components/Markdown'
10
- import { useChat, useChatMessages, useCurrentChat, useCurrentChatMessages, useWidget } from '../../context/hooks'
10
+ import { useChat, useChatMessages, useCurrentChat, useCurrentChatMessages, useIsLastEntryInCurrentChat, 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,7 +17,6 @@ interface Props {
17
17
  steps: ChatStep[],
18
18
  messageId: number,
19
19
  chatId: string,
20
- userHasAlreadyAnswered?: boolean,
21
20
  }
22
21
 
23
22
  interface StepChatStepWithTarget extends Omit<StepChatStep, 'status' | 'id' | 'type'> {
@@ -152,7 +151,7 @@ const AwaitingApproval = ({ customApproveText, chatId }: { chatId: string, custo
152
151
  const chat = useChat(chatId)
153
152
 
154
153
  const onAnswer = (response: string) => {
155
- chat.pushMessage(ChatEntry.createUserEntry('', false, undefined, undefined, response))
154
+ chat.pushMessage(ChatEntry.createUserEntry(response))
156
155
  }
157
156
 
158
157
  return <>
@@ -174,10 +173,11 @@ const AwaitingApproval = ({ customApproveText, chatId }: { chatId: string, custo
174
173
  </>
175
174
  }
176
175
 
177
- export const ToolStepsList = ({ toolStep, messageId, chatId }: { toolStep: ToolChatStep, messageId: number, chatId: string }) => {
176
+ const ToolStepsList = ({ toolStep, messageId, chatId }: { toolStep: ToolChatStep, messageId: number, chatId: string }) => {
178
177
  const t = useTranslate(dictionary)
179
178
  const chat = useCurrentChat()
180
179
  const messages = useCurrentChatMessages()
180
+ const isLastMessage = useIsLastEntryInCurrentChat(messageId)
181
181
  const inputParsed = `\`\`\`json
182
182
  ${JSON.stringify(toolStep?.input, null, 2)}
183
183
  \`\`\``
@@ -222,15 +222,16 @@ export const ToolStepsList = ({ toolStep, messageId, chatId }: { toolStep: ToolC
222
222
  </Markdown>
223
223
  </Accordion>
224
224
 
225
- <AwaitingApproval customApproveText={t.approveTool} chatId={chatId} />
225
+ {isLastMessage && <AwaitingApproval customApproveText={t.approveTool} chatId={chatId} />}
226
226
  </Card>
227
227
  </div>
228
228
  </AnimatedHeight> : null}
229
229
  </>
230
230
  }
231
231
 
232
- export const StepsList = ({ steps, messageId, chatId, userHasAlreadyAnswered }: Props) => {
232
+ export const StepsList = ({ steps, messageId, chatId }: Props) => {
233
233
  const t = useTranslate(dictionary)
234
+ const isLastMessage = useIsLastEntryInCurrentChat(messageId)
234
235
 
235
236
  const filteredSteps = steps.filter(s => s.type === 'step')
236
237
  const actualSteps = useMemo(() => filteredSteps.filter((item) => {
@@ -302,7 +303,7 @@ export const StepsList = ({ steps, messageId, chatId, userHasAlreadyAnswered }:
302
303
  <Column gap="8px" mt="8px">
303
304
  <Divider colorScheme="light" />
304
305
  <Text color="light.700">{planning?.[0]?.user_question}</Text>
305
- {!userHasAlreadyAnswered && planning?.[0]?.status === 'awaiting_approval' && <AwaitingApproval chatId={chatId} />}
306
+ {isLastMessage && planning?.[0]?.status === 'awaiting_approval' && <AwaitingApproval chatId={chatId} />}
306
307
  </Column>
307
308
 
308
309
  {planning?.[0]?.status === 'success' && <ViewToolsDetails chatId={chatId} messageId={messageId}/>}
@@ -310,8 +311,7 @@ export const StepsList = ({ steps, messageId, chatId, userHasAlreadyAnswered }:
310
311
  </div>
311
312
  </AnimatedHeight> : null}
312
313
 
313
- {toolsStep && toolsStep.status === 'awaiting_approval' && !userHasAlreadyAnswered &&
314
- <ToolStepsList toolStep={toolsStep} messageId={messageId} chatId={chatId} />}
314
+ {toolsStep && toolsStep.status === 'awaiting_approval' && <ToolStepsList toolStep={toolsStep} messageId={messageId} chatId={chatId} />}
315
315
  </>
316
316
  )
317
317
  }
@@ -1,4 +1,5 @@
1
- import { agentToolsClient, aiClient, workspaceClient } from '@stack-spot/portal-network'
1
+ import { agentToolsClient, aiClient, AnswerChatStep, ChatAgentTool, ChatStep, PlanningChatStep, workspaceClient } from '@stack-spot/portal-network'
2
+ import { AgentModel } from '@stack-spot/portal-network/api/agent-tools'
2
3
  import { last } from 'lodash'
3
4
  import { ChatEntry } from '../../state/ChatEntry'
4
5
  import { ChatProperties, ChatState } from '../../state/ChatState'
@@ -41,6 +42,96 @@ export async function findWorkspace(id: string | null): Promise<ChatProperties['
41
42
  }
42
43
  }
43
44
 
45
+ function toJSONString(data: any) {
46
+ if (data === undefined || data === null) return undefined
47
+ if (typeof data === 'object') {
48
+ try {
49
+ return JSON.stringify(data, null, 2)
50
+ } catch { /* empty */ }
51
+ } else {
52
+ try {
53
+ return JSON.stringify(JSON.parse(data), null, 2)
54
+ } catch { /* empty */ }
55
+ }
56
+ return `${data}`
57
+ }
58
+
59
+ function findTool(agent: AgentModel | undefined, id: string) {
60
+ const allToolkits = [
61
+ ...agent?.toolkits?.builtin_toolkits ?? [],
62
+ ...agent?.toolkits?.custom_toolkits ?? [],
63
+ ...agent?.toolkits?.mcp_toolkits ?? [],
64
+ ]
65
+ for (const toolkit of allToolkits) {
66
+ for (const tool of toolkit.tools ?? []) {
67
+ if ('id' in tool && tool.id === id) return { toolkit, tool }
68
+ }
69
+ }
70
+ return { toolkit: undefined, tool: { name: id } }
71
+ }
72
+
73
+ async function stepsFromAgentInfo(
74
+ agentInfo: any[] | null | undefined, agentId: string | undefined, isLastEntry: boolean,
75
+ ): Promise<ChatStep[] | undefined> {
76
+ const planningInfo = agentInfo?.find(i => i.type === 'planning' && !!i.data)
77
+ if (planningInfo) {
78
+ let agent: AgentModel | undefined
79
+ try {
80
+ agent = agentId ? await agentToolsClient.agent.query({ agentId }) : undefined
81
+ } catch { /* empty */ }
82
+ const planning: PlanningChatStep = {
83
+ type: 'planning',
84
+ goal: planningInfo.data.plan_goal,
85
+ status: planningInfo.action === 'awaiting_approval' ? 'awaiting_approval' : 'success',
86
+ id: `${Math.random()}`,
87
+ steps: planningInfo.data.steps,
88
+ duration: planningInfo.duration,
89
+ user_question: planningInfo.data.user_question,
90
+ }
91
+ const steps = planningInfo?.data?.steps?.map((s: any): ChatStep => {
92
+ const lastTool = agentInfo!.find(i => i.id === last(s.tools as any[])?.tool_execution_id && i.action === 'end')
93
+ return {
94
+ type: 'step',
95
+ id: s.id,
96
+ status: isLastEntry && planningInfo.action === 'awaiting_approval' ? 'pending' : 'success',
97
+ input: s.goal,
98
+ output: toJSONString(lastTool?.data?.output),
99
+ duration: lastTool?.duration,
100
+ attempts: [{
101
+ tools: s.tools?.map((t: any): ChatAgentTool => {
102
+ const { toolkit, tool } = findTool(agent, t.tool_id)
103
+ const start = agentInfo!.find(i => i.id === t.tool_execution_id && i.action === 'start')
104
+ const end = agentInfo!.find(i => i.id === t.tool_execution_id && i.action === 'end')
105
+ return {
106
+ id: t.tool_id,
107
+ executionId: t.tool_execution_id,
108
+ name: tool?.name,
109
+ description: 'description' in tool ? tool.description : undefined,
110
+ image: toolkit ? (('image_url' in toolkit ? toolkit?.image_url : toolkit?.avatar) ?? undefined) : undefined,
111
+ goal: t.goal,
112
+ duration: end?.duration,
113
+ input: toJSONString(start?.data?.input),
114
+ output: toJSONString(end?.data?.output),
115
+ }
116
+ }),
117
+ }],
118
+ }
119
+ })
120
+ const answerInfo = agentInfo?.find(i => i.type === 'chat' && i.action === 'end')
121
+ const answer: AnswerChatStep | undefined = answerInfo ? {
122
+ id: `${Math.random()}`,
123
+ status: 'success',
124
+ type: 'answer',
125
+ duration: answerInfo.duration,
126
+ } : undefined
127
+ return answer ? [planning, ...steps, answer] : [planning, ...steps]
128
+ }
129
+ }
130
+
131
+ function toolsFromAgentInfo(agentInfo: any[] | null | undefined): string[] {
132
+ return agentInfo?.find(i => i.type === 'chat' && i.action === 'end')?.data?.used_tools
133
+ }
134
+
44
135
  /**
45
136
  * Loads the chat identified by `conversationId` into the widget passed as parameter.
46
137
  * @param widget
@@ -64,7 +155,7 @@ export async function loadChat(widget: WidgetState, conversationId: string) {
64
155
  initial: { features: widget.chatFeatures, label: chat.title, stack, workspace, agent: agent ? {
65
156
  ...agent, builtIn } : undefined },
66
157
  interceptors: widget.interceptors,
67
- entries: chat.history?.map(item => new ChatEntry({
158
+ entries: await Promise.all(chat.history?.map(async (item, index) => new ChatEntry({
68
159
  agentType: item.agent === 'USER' ? 'user' : 'bot',
69
160
  content: item.content,
70
161
  type: 'md',
@@ -72,7 +163,9 @@ export async function loadChat(widget: WidgetState, conversationId: string) {
72
163
  messageId: item.message_id,
73
164
  knowledgeSources: item.agent === 'USER' ? undefined : genericSourcesToKnowledgeSources(item.sources),
74
165
  updated: item.updated,
75
- })),
166
+ steps: await stepsFromAgentInfo(item.agent_info, item.custom_agent?.id, index === chat.history!.length - 1),
167
+ tools: toolsFromAgentInfo(item.agent_info),
168
+ })) ?? []),
76
169
  }))
77
170
  widget.chatTabs.select(chat.id)
78
171
  }