@stack-spot/ai-chat-widget 3.5.0-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 (79) hide show
  1. package/CHANGELOG.md +77 -0
  2. package/dist/app-metadata.json +3 -3
  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 -18
  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 +9 -10
  17. package/dist/state/ChatEntry.d.ts.map +1 -1
  18. package/dist/state/ChatEntry.js +2 -16
  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.map +1 -1
  35. package/dist/views/Chat/ChatMessage.js +19 -21
  36. package/dist/views/Chat/ChatMessage.js.map +1 -1
  37. package/dist/views/Chat/StepsList.d.ts +2 -8
  38. package/dist/views/Chat/StepsList.d.ts.map +1 -1
  39. package/dist/views/Chat/StepsList.js +7 -6
  40. package/dist/views/Chat/StepsList.js.map +1 -1
  41. package/dist/views/ChatHistory/utils.js +2 -2
  42. package/dist/views/ChatHistory/utils.js.map +1 -1
  43. package/dist/views/KnowledgeSources.d.ts +1 -1
  44. package/dist/views/KnowledgeSources.d.ts.map +1 -1
  45. package/dist/views/KnowledgeSources.js +31 -45
  46. package/dist/views/KnowledgeSources.js.map +1 -1
  47. package/dist/views/MessageInput/QuickCommandSelector.d.ts.map +1 -1
  48. package/dist/views/MessageInput/QuickCommandSelector.js +29 -52
  49. package/dist/views/MessageInput/QuickCommandSelector.js.map +1 -1
  50. package/dist/views/MessageInput/index.d.ts.map +1 -1
  51. package/dist/views/MessageInput/index.js +9 -1
  52. package/dist/views/MessageInput/index.js.map +1 -1
  53. package/dist/views/MessageInput/styled.d.ts.map +1 -1
  54. package/dist/views/MessageInput/styled.js +4 -0
  55. package/dist/views/MessageInput/styled.js.map +1 -1
  56. package/dist/views/Resources.js +8 -5
  57. package/dist/views/Resources.js.map +1 -1
  58. package/dist/views/Tools.js +1 -1
  59. package/dist/views/Tools.js.map +1 -1
  60. package/package.json +2 -2
  61. package/src/app-metadata.json +3 -3
  62. package/src/chat-interceptors/quick-commands.ts +2 -2
  63. package/src/chat-interceptors/send-message.ts +8 -20
  64. package/src/components/form/DescribedCheckboxGroup.tsx +61 -35
  65. package/src/context/hooks.ts +24 -0
  66. package/src/state/ChatEntry.ts +10 -18
  67. package/src/state/ChatState.ts +16 -0
  68. package/src/utils/tools.ts +28 -17
  69. package/src/views/Agents/AgentDescription.tsx +40 -36
  70. package/src/views/Agents/AgentsTab.tsx +8 -7
  71. package/src/views/Chat/ChatMessage.tsx +21 -23
  72. package/src/views/Chat/StepsList.tsx +8 -8
  73. package/src/views/ChatHistory/utils.ts +2 -2
  74. package/src/views/KnowledgeSources.tsx +57 -77
  75. package/src/views/MessageInput/QuickCommandSelector.tsx +39 -93
  76. package/src/views/MessageInput/index.tsx +10 -0
  77. package/src/views/MessageInput/styled.ts +4 -0
  78. package/src/views/Resources.tsx +11 -10
  79. package/src/views/Tools.tsx +1 -1
@@ -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,8 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
357
355
  widget.set('panel', 'resources')
358
356
  }
359
357
 
360
- const shouldRender = (entry.content || entry.error || (message.isDone() && !!entry.steps?.length) || !!entry.upload?.length)
361
- && (!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
362
360
 
363
361
  return shouldRender && (
364
362
  <li key={entry.messageId} className={entry.agentType} ref={ref}>
@@ -374,8 +372,7 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
374
372
  {entry.badges.map((b, index) => <Badge key={index} colorPalette={b.color ?? 'cyan'} appearance="square">{b.label}</Badge>)}
375
373
  </div>}
376
374
 
377
- {!!entry.steps?.length && <StepsList steps={entry.steps} chatId={chat.id} messageId={message.id}
378
- userHasAlreadyAnswered={userHasAlreadyAnswered} />}
375
+ {!!entry.steps?.length && <StepsList steps={entry.steps} chatId={chat.id} messageId={message.id} />}
379
376
 
380
377
  {renderContent()}
381
378
 
@@ -396,7 +393,7 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
396
393
  ))}</ul>
397
394
  </div>}
398
395
 
399
- {(!!agentsTools?.length || !!entry.tools?.length) &&
396
+ {showToolBox &&
400
397
  <div className="tools-box">
401
398
  <Button appearance="none" onClick={openResourcesPanel} aria-label={t.openResourcesPanel}>
402
399
  {agentsTools?.map((agent) => (
@@ -411,13 +408,14 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
411
408
  </ImageBox>
412
409
  ))}
413
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.
414
412
  if (agentsTools?.some((agent) => agent.id === id)) return null
415
- const tool = toolById(id, toolKits)
413
+ const tool = toolById(id, toolkits)
416
414
  return (
417
415
  <ImageBox key={id} className="agent-info-avatar-resource">
418
416
  <ImageWithFallback
419
417
  src={tool?.image}
420
- fallback={<Icon icon="Cog" />}
418
+ fallback={<Icon icon="Cog" title={tool?.name} aria-label={tool?.name} />}
421
419
  alt={tool?.name}
422
420
  aria-label={tool?.name}
423
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'> {
@@ -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
  }
@@ -64,7 +64,7 @@ function findTool(agent: AgentModel | undefined, id: string) {
64
64
  ]
65
65
  for (const toolkit of allToolkits) {
66
66
  for (const tool of toolkit.tools ?? []) {
67
- if (tool.id === id) return { toolkit, tool }
67
+ if ('id' in tool && tool.id === id) return { toolkit, tool }
68
68
  }
69
69
  }
70
70
  return { toolkit: undefined, tool: { name: id } }
@@ -106,7 +106,7 @@ async function stepsFromAgentInfo(
106
106
  id: t.tool_id,
107
107
  executionId: t.tool_execution_id,
108
108
  name: tool?.name,
109
- description: tool?.description,
109
+ description: 'description' in tool ? tool.description : undefined,
110
110
  image: toolkit ? (('image_url' in toolkit ? toolkit?.image_url : toolkit?.avatar) ?? undefined) : undefined,
111
111
  goal: t.goal,
112
112
  duration: end?.duration,
@@ -1,12 +1,11 @@
1
- import { Box } from '@citric/core'
2
1
  import { Button, Tab } from '@stack-spot/citric-react'
3
2
  import { Placeholder } from '@stack-spot/portal-components/Placeholder'
4
3
  import { aiClient, dataIntegrationClient, workspaceAiClient } from '@stack-spot/portal-network'
5
4
  import { VisibilityLevelEnum } from '@stack-spot/portal-network/api/ai'
5
+ import { KnowledgeSourceItemResponse } from '@stack-spot/portal-network/api/dataIntegration'
6
6
  import { WorkspaceResponse } from '@stack-spot/portal-network/api/workspace-ai'
7
7
  import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
8
8
  import React, { useCallback, useEffect, useMemo, useRef } from 'react'
9
- import InfiniteScroll from 'react-infinite-scroll-component'
10
9
  import { NavigationComponent } from '../components/ComponentNavigator'
11
10
  import { DescribedCheckboxGroup } from '../components/form/DescribedCheckboxGroup'
12
11
  import { RightPanelContentList } from '../components/RightPanelContentList'
@@ -66,13 +65,19 @@ const KnowledgeSourcesPanel = () => {
66
65
  }, [chat])
67
66
 
68
67
  const allTabsMap: Partial<Record<Scope, Omit<Tab, 'key'>>> = {
69
- favorite: { label: t.favorites,
70
- content: <KnowledgeSourcesTab key="favorite" visibility="favorite" allKS={allKS} onSubmit={onSubmit} /> },
71
- personal: { label: t.personal,
72
- content: <KnowledgeSourcesTab key="personal" visibility="personal" allKS={allKS} onSubmit={onSubmit} /> },
68
+ favorite: {
69
+ label: t.favorites,
70
+ content: <KnowledgeSourcesTab key="favorite" visibility="favorite" allKS={allKS} onSubmit={onSubmit} />,
71
+ },
72
+ personal: {
73
+ label: t.personal,
74
+ content: <KnowledgeSourcesTab key="personal" visibility="personal" allKS={allKS} onSubmit={onSubmit} />,
75
+ },
73
76
  shared: { label: t.shared, content: <KnowledgeSourcesTab key="shared" visibility="shared" allKS={allKS} onSubmit={onSubmit} /> },
74
- workspace: { label: t.spots,
75
- content: <KnowledgeSourcesTabWorkspace key="workspace" visibility="workspace" allKS={allKS} onSubmit={onSubmit} /> },
77
+ workspace: {
78
+ label: t.spots,
79
+ content: <KnowledgeSourcesTabWorkspace key="workspace" visibility="workspace" allKS={allKS} onSubmit={onSubmit} />,
80
+ },
76
81
  account: { label: t.account, content: <KnowledgeSourcesTab key="account" visibility="account" allKS={allKS} onSubmit={onSubmit} /> },
77
82
  }
78
83
 
@@ -97,47 +102,29 @@ const KnowledgeSourcesPanel = () => {
97
102
 
98
103
  export const KnowledgeSourcesTab = ({ visibility, allKS, onSubmit, workspaceId, showSubmitButton = true }: TabProps) => {
99
104
  const t = useTranslate(dictionary)
100
-
101
- let knowledgeSources: any[] = []
102
- let fetchNextPage: (() => void) | undefined
103
- let hasNextPage: boolean | undefined
104
- let isLoadingKnowledgeSources: boolean | undefined
105
- let ksListError: any
106
-
107
- if (workspaceId) {
108
- knowledgeSources = workspaceAiClient.getKSFromWorkspaceAi.useQuery({ workspaceId })
109
- fetchNextPage = undefined
110
- hasNextPage = false
111
- isLoadingKnowledgeSources = false
112
- ksListError = undefined
113
- } else {
114
- const [
115
- ksListPages,
116
- _isLoadingKnowledgeSources,
117
- _ksListError,
118
- { fetchNextPage: _fetchNextPage, hasNextPage: _hasNextPage },
119
- ] = dataIntegrationClient.knowledgeSourcesV2.useStatefulInfiniteQuery(
120
- {
121
- order: 'a-to-z',
122
- visibilityList: visibility ? [visibility] : [],
123
- types: ['snippet', 'api', 'event', 'custom'],
124
- size: 20,
125
- },
126
- { enabled: true },
127
- )
128
- knowledgeSources = ksListPages?.flat() ?? []
129
- fetchNextPage = _fetchNextPage
130
- hasNextPage = _hasNextPage
131
- isLoadingKnowledgeSources = _isLoadingKnowledgeSources
132
- ksListError = _ksListError
133
- }
105
+ const [data = []] = workspaceAiClient.getKSFromWorkspaceAi.useStatefulQuery(
106
+ { workspaceId: workspaceId! }, { enabled: !!workspaceId })
107
+ const workspaceKs = data as KnowledgeSourceItemResponse[]
108
+
109
+ const [ksListPages = [], { fetchNextPage, hasNextPage }] = dataIntegrationClient.knowledgeSourcesV2.useInfiniteQuery(
110
+ {
111
+ order: 'a-to-z',
112
+ visibilityList: visibility ? [visibility] : [],
113
+ types: ['snippet', 'api', 'event', 'custom'],
114
+ size: 20,
115
+ page: 1,
116
+ },
117
+ { enabled: !workspaceId },
118
+ )
119
+ const knowledgeSources = workspaceId ? workspaceKs : ksListPages
134
120
 
135
121
  const initialValue = useMemo(() => {
136
122
  const currentlySelected = allKS.current?.map(ks => ks.id)
137
- return knowledgeSources.filter(ks => currentlySelected?.includes(ks.slug))
138
- }, [knowledgeSources, allKS])
123
+ return knowledgeSources?.filter((ks: KnowledgeSourceItemResponse) => currentlySelected?.includes(ks.slug))
124
+ }, [knowledgeSources])
125
+
126
+ const listFavorites = dataIntegrationClient.knowledgeSourcesV2.useQuery({ visibilityList: ['favorite'] })?.items
139
127
 
140
- const listFavorites = dataIntegrationClient.knowledgeSources.useQuery({ visibility: 'favorite' })
141
128
  const [addFavorite, pendingAddFav] = dataIntegrationClient.addFavoriteKnowledgeSource.useMutation()
142
129
  const [removeFavorite, pendingRemoveFav] = dataIntegrationClient.removeFavoriteKnowledgeSource.useMutation()
143
130
 
@@ -145,7 +132,7 @@ export const KnowledgeSourcesTab = ({ visibility, allKS, onSubmit, workspaceId,
145
132
  try {
146
133
  await removeFavorite({ slug: idOrSlug })
147
134
  await aiClient.knowledgeSources.invalidate()
148
- await dataIntegrationClient.knowledgeSources.invalidate()
135
+ await dataIntegrationClient.knowledgeSourcesV2.invalidate()
149
136
  } catch (error) {
150
137
  // eslint-disable-next-line no-console
151
138
  console.error(error)
@@ -156,7 +143,7 @@ export const KnowledgeSourcesTab = ({ visibility, allKS, onSubmit, workspaceId,
156
143
  const onRemoveFavorite = (idOrSlug: string) => new Promise<boolean>(async (resolve, reject) => {
157
144
  try {
158
145
  await removeFavoriteKs(idOrSlug)
159
- if (!pendingRemoveFav){
146
+ if (!pendingRemoveFav) {
160
147
  resolve(true)
161
148
  }
162
149
 
@@ -164,17 +151,14 @@ export const KnowledgeSourcesTab = ({ visibility, allKS, onSubmit, workspaceId,
164
151
  // eslint-disable-next-line no-console
165
152
  console.error(error)
166
153
  reject(error)
167
- }
154
+ }
168
155
  })
169
156
 
170
- if (isLoadingKnowledgeSources && knowledgeSources.length === 0) {return null}
171
- if (ksListError) {return null}
172
-
173
157
  const addFavoriteKs = async (idOrSlug: string) => {
174
158
  try {
175
159
  await addFavorite({ slug: idOrSlug })
176
160
  await aiClient.knowledgeSources.invalidate()
177
- await dataIntegrationClient.knowledgeSources.invalidate()
161
+ await dataIntegrationClient.knowledgeSourcesV2.invalidate()
178
162
  } catch (error) {
179
163
  // eslint-disable-next-line no-console
180
164
  console.error(error)
@@ -185,7 +169,7 @@ export const KnowledgeSourcesTab = ({ visibility, allKS, onSubmit, workspaceId,
185
169
  const onAddFavorite = (idOrSlug: string) => new Promise<boolean>(async (resolve, reject) => {
186
170
  try {
187
171
  await addFavoriteKs(idOrSlug)
188
- if (!pendingAddFav){
172
+ if (!pendingAddFav) {
189
173
  resolve(true)
190
174
  }
191
175
 
@@ -193,31 +177,27 @@ export const KnowledgeSourcesTab = ({ visibility, allKS, onSubmit, workspaceId,
193
177
  // eslint-disable-next-line no-console
194
178
  console.error(error)
195
179
  reject(error)
196
- }
180
+ }
197
181
  })
198
182
 
199
- return (
200
- <>
201
- <Box className="content" id="ks-scrollable" style={{ overflow: 'auto', height: '100%' }}>
202
- <InfiniteScroll
203
- scrollableTarget="ks-scrollable"
204
- dataLength={knowledgeSources.length}
205
- next={fetchNextPage ?? (() => {})}
206
- hasMore={hasNextPage}
207
- loader={isLoadingKnowledgeSources}
208
- >
209
- <DescribedCheckboxGroup
210
- options={knowledgeSources}
211
- initialValue={initialValue}
212
- globalSelection={allKS}
213
- data={ks => ({ idOrSlug: ks.slug, description: ks.description, name: ks.name, listFavorites, onAddFavorite, onRemoveFavorite })}
214
- emptyResults={<Placeholder title={t.noSearchResults} description={t.noSearchResultsDescription} />}
215
- emptyDataset={<Placeholder title={t.noData} description={t.noDataDescription} className="no-data-placeholder" />}
216
- />
217
- </InfiniteScroll>
218
- </Box>
219
- {!!knowledgeSources.length && showSubmitButton && <Button onClick={onSubmit}>{t.apply}</Button>}
220
- </>
183
+ return (<>
184
+ <div className="content">
185
+ <DescribedCheckboxGroup
186
+ options={knowledgeSources}
187
+ initialValue={initialValue}
188
+ globalSelection={allKS}
189
+ hasNextPage={hasNextPage && !workspaceId}
190
+ fetchNextPage={fetchNextPage}
191
+ data={(ks: KnowledgeSourceItemResponse) => ({
192
+ idOrSlug: ks.slug, description: ks.description, name: ks.name, listFavorites,
193
+ onAddFavorite, onRemoveFavorite,
194
+ })}
195
+ emptyResults={<Placeholder title={t.noSearchResults} description={t.noSearchResultsDescription} />}
196
+ emptyDataset={<Placeholder title={t.noData} description={t.noDataDescription} className="no-data-placeholder" />}
197
+ />
198
+ </div>
199
+ {!!knowledgeSources?.length && showSubmitButton && <Button onClick={onSubmit}>{t.apply}</Button>}
200
+ </>
221
201
  )
222
202
  }
223
203
 
@@ -228,7 +208,7 @@ export function KnowledgeSourcesTabWorkspace({ allKS, onSubmit }: TabProps) {
228
208
  component: 'ks',
229
209
  props: { visibility: 'workspace', workspaceId: workspace.id, allKS, onSubmit },
230
210
  })
231
-
211
+
232
212
  return <WorkspaceTabNavigator components={workspaceTabComponents} getNavigateParam={buildNavigateParams} />
233
213
  }
234
214
 
@@ -1,9 +1,7 @@
1
- import { Box } from '@citric/core'
2
1
  import { Icon } from '@stack-spot/citric-icons'
3
2
  import { aiClient, workspaceAiClient } from '@stack-spot/portal-network'
4
3
  import { QuickCommandResponse } from '@stack-spot/portal-network/api/ai'
5
- import { useCallback, useMemo } from 'react'
6
- import InfiniteScroll from 'react-infinite-scroll-component'
4
+ import { useCallback } from 'react'
7
5
  import { Selector } from '../../components/Selector'
8
6
  import { useCurrentChat, useCurrentChatState, useWidgetState } from '../../context/hooks'
9
7
  import { quickCommandRegex } from '../../regex'
@@ -79,103 +77,51 @@ export const QuickCommandSelector = ({ inputRef, isTrial }:
79
77
  inputRef.current.focus()
80
78
  }, [chat, inputRef])
81
79
 
82
- const [quickCommandsPages = [], isLoadingQuickCommands, _qcListError, infiniteQueryResult,
83
- ] = aiClient.allQuickCommandsV3.useStatefulInfiniteQuery(
84
- {
85
- size: 20,
86
- page: 1,
87
- order: 'a-to-z',
88
- },
89
- { enabled: !spotId },
90
- )
91
-
92
- const quickCommands = useMemo(() => quickCommandsPages, [quickCommandsPages])
93
-
94
- const quickCommandsFiltered = useMemo(() => quickCommands.filter(
95
- (qc) => qc.visibility_level.toLowerCase() !== 'workspace'), [quickCommands],
96
- )
97
-
98
- const workspaceQuickCommands = workspaceAiClient.workspacesContentsByType.useQuery({
99
- contentType: 'quick_command',
100
- }) || []
101
-
102
- const workspaceQuickCommandsWithWorkspaceName: QuickCommandResponseWithSpaceName[] =
103
- useMemo(
104
- () =>
105
- workspaceQuickCommands.flatMap(({ qcs, space_name }) =>
106
- qcs?.map((qc) => ({ ...qc, spaceName: space_name })),
107
- ) as QuickCommandResponseWithSpaceName[],
108
- [workspaceQuickCommands],
80
+ const getQuickCommands = () => {
81
+ if (spotId) {
82
+ return workspaceAiClient.getQCFromWorkspaceAi.useQuery({ workspaceId: spotId })
83
+ }
84
+
85
+ const quickCommands = aiClient.allQuickCommands.useQuery({ order: 'a-to-z' })
86
+ const quickCommandsFiltered = quickCommands.filter(
87
+ (qc) => qc.visibility_level.toLowerCase() !== 'workspace',
109
88
  )
110
89
 
111
- const allQuickCommands =
112
- spotId
113
- ? workspaceAiClient.getQCFromWorkspaceAi.useQuery({ workspaceId: spotId }) || []
114
- : [...quickCommandsFiltered, ...workspaceQuickCommandsWithWorkspaceName]
90
+ const workspaceQuickCommands = workspaceAiClient.workspacesContentsByType.useQuery({
91
+ contentType: 'quick_command',
92
+ })
115
93
 
116
- const fetchNextPage = !spotId ? infiniteQueryResult?.fetchNextPage : undefined
117
- const hasNextPage = !spotId ? infiniteQueryResult?.hasNextPage : false
94
+ const workspaceQuickCommandsWithWorkspaceName: QuickCommandResponseWithSpaceName[] =
95
+ workspaceQuickCommands.flatMap(({ qcs, space_name }) =>
96
+ qcs?.map((qc) => ({ ...qc, spaceName: space_name })),
97
+ ) as QuickCommandResponseWithSpaceName[]
118
98
 
119
- const QuickCommandItem = ({ slug, description, spaceName }: QuickCommandResponseWithSpaceName) => (<>
99
+ return [...quickCommandsFiltered, ...workspaceQuickCommandsWithWorkspaceName]
100
+ }
101
+
102
+ const QuickCommandItem = ({ slug, description, spaceName }: QuickCommandResponseWithSpaceName) => <>
120
103
  <p className="selector-description">{spaceName}</p>
121
104
  <p className="selector-title">/{slug?.toUpperCase()}</p>
122
105
  <p className="selector-description">{description}</p>
123
106
  </>
124
- )
125
-
126
- if (spotId) {
127
- return (
128
- <Selector
129
- inputRef={inputRef}
130
- favorite={{
131
- useFavorites, onAddFavorite, onRemoveFavorite, favoriteIsSlug: true,
132
- }}
133
- selectorConfig={{
134
- resourceName: 'Quick Command',
135
- shortcut: '/',
136
- icon: <Icon icon="QuickCommand" />,
137
- searchProp: 'slug',
138
- urlBuilder: (qc) => `/quick-command/${qc?.slug}`,
139
- regex: quickCommandRegex,
140
- sections: isTrial ? ['favorite', 'personal'] : ['favorite', 'personal', 'workspace', 'account', 'shared'],
141
- isEnabled: isQuickCommandEnabled,
142
- onSelect: onSelectItem,
143
- renderComponentItem: QuickCommandItem,
144
- useData: () => allQuickCommands,
145
- }}
146
- />
147
- )
148
- }
149
107
 
150
- return (
151
- <Box className="content" id="qc-scrollable" style={{ overflow: 'auto', height: '100%' }}>
152
- <InfiniteScroll
153
- scrollableTarget="qc-scrollable"
154
- dataLength={allQuickCommands.length}
155
- next={fetchNextPage as any}
156
- hasMore={!!hasNextPage}
157
- loader={isLoadingQuickCommands}
158
- >
159
- <Selector
160
- inputRef={inputRef}
161
- favorite={{
162
- useFavorites, onAddFavorite, onRemoveFavorite, favoriteIsSlug: true,
163
- }}
164
- selectorConfig={{
165
- resourceName: 'Quick Command',
166
- shortcut: '/',
167
- icon: <Icon icon="QuickCommand" />,
168
- searchProp: 'slug',
169
- urlBuilder: (qc) => `/quick-command/${qc?.slug}`,
170
- regex: quickCommandRegex,
171
- sections: isTrial ? ['favorite', 'personal'] : ['favorite', 'personal', 'workspace', 'account', 'shared'],
172
- isEnabled: isQuickCommandEnabled,
173
- onSelect: onSelectItem,
174
- renderComponentItem: QuickCommandItem,
175
- useData: () => allQuickCommands,
176
- }}
177
- />
178
- </InfiniteScroll>
179
- </Box>
180
- )
108
+ return <Selector
109
+ inputRef={inputRef}
110
+ favorite={{
111
+ useFavorites, onAddFavorite, onRemoveFavorite, favoriteIsSlug: true,
112
+ }}
113
+ selectorConfig={{
114
+ resourceName: 'Quick Command',
115
+ shortcut: '/',
116
+ icon: <Icon icon="QuickCommand" />,
117
+ searchProp: 'slug',
118
+ urlBuilder: (qc) => `/quick-command/${qc?.slug}`,
119
+ regex: quickCommandRegex,
120
+ sections: isTrial ? ['favorite', 'personal'] : ['favorite', 'personal', 'workspace', 'account', 'shared'],
121
+ isEnabled: isQuickCommandEnabled,
122
+ onSelect: onSelectItem,
123
+ renderComponentItem: QuickCommandItem,
124
+ useData: getQuickCommands,
125
+ }}
126
+ />
181
127
  }
@@ -39,6 +39,7 @@ export const MessageInput = ({ chatWindowRef, customInputMessage }:
39
39
  const { handleKeyDown, handleKeyUp } = useUserEntryHistoryShortcut()
40
40
  const isTrial = checkIsTrial()
41
41
  const { isDragging, handleDrop, handleDragLeave } = useUploadDragDrop()
42
+ const [disabled, setDisabled] = useState(false)
42
43
 
43
44
  usePasteUpload({
44
45
  textAreaRef,
@@ -88,6 +89,14 @@ export const MessageInput = ({ chatWindowRef, customInputMessage }:
88
89
  }, [chat, t])
89
90
 
90
91
  const onSend = useCallback(async () => {
92
+ if (disabled) return
93
+
94
+ if (chat.get('isLoading')) {
95
+ setDisabled(true)
96
+ await chat.whenLoadingEnds()
97
+ setDisabled(false)
98
+ }
99
+
91
100
  const message = chat.get('nextMessage')
92
101
  const code = chat.get('codeSelection')
93
102
  const language = chat.get('codeLanguage')
@@ -162,6 +171,7 @@ export const MessageInput = ({ chatWindowRef, customInputMessage }:
162
171
  onKeyUp={handleKeyUp}
163
172
  onIncreaseSize={() => setExpanded(false)}
164
173
  onResetSize={() => !expansionLocked.current && setExpanded(true)}
174
+ disabled={disabled}
165
175
  />
166
176
  </div>
167
177
  </div>
@@ -301,6 +301,10 @@ export const MessageInputBox = styled.div`
301
301
  &:focus {
302
302
  box-shadow: none !important;
303
303
  }
304
+ &:disabled {
305
+ background-color: transparent !important;
306
+ opacity: 0.6;
307
+ }
304
308
  }
305
309
  `
306
310
 
@@ -46,29 +46,30 @@ const ResourcesPanel = () => {
46
46
  const chat = widget.chatTabs.getAll().find(c => c.id === chatId)
47
47
  return chat?.getMessages().find(m => m.id === messageId)?.getValue()
48
48
  }, [messageId])
49
- const [toolKits] = agentToolsClient.tools.useStatefulQuery({}, { enabled: !!message?.agent?.id })
50
49
  const [agent] = agentToolsClient.agent.useStatefulQuery({ agentId: message?.agent?.id || '' },
51
50
  { enabled: !!message?.agent?.id })
52
- const tools = useMemo(() => message?.tools?.map(id => toolById(id, toolKits)), [messageId, toolKits])
53
- const customTools = useMemo(() => message?.tools?.map(id => toolById(id, agent?.toolkits?.custom_toolkits)),
54
- [messageId, agent?.toolkits?.custom_toolkits])
55
-
51
+ const toolkits = useMemo(() => [
52
+ ...agent?.toolkits?.builtin_toolkits ?? [],
53
+ ...agent?.toolkits?.custom_toolkits ?? [],
54
+ ...agent?.toolkits?.mcp_toolkits ?? [],
55
+ ], [agent])
56
+ const tools = useMemo(() => message?.tools?.map(id => toolById(id, toolkits)), [messageId, toolkits])
56
57
  const [agentsTools] = agentToolsClient.agentsByIds.useStatefulQuery({ searchAgentsRequest:{ ids: message?.tools || [] } })
57
- const hasAgentTool = useMemo(() => message?.tools?.some(id => agentsTools?.find((agent) => agent.id === id)), [messageId, toolKits])
58
+ const hasAgentTool = useMemo(() => message?.tools?.some(id => agentsTools?.find((agent) => agent.id === id)), [messageId])
58
59
 
59
60
  const header = (image?: string, label?: string) => (
60
61
  <Row>
61
- <ImageBox >
62
+ <ImageBox>
62
63
  <ImageWithFallback src={image} fallback={<Icon icon="Agent" />} aria-label={label} title={label} />
63
64
  </ImageBox>
64
65
  <Text style={{ marginLeft: '8px' }}>{label}</Text>
65
66
  </Row>
66
67
  )
67
-
68
- return !!(tools?.length || customTools?.length) && (
68
+
69
+ return !!tools?.length && (
69
70
  <>
70
71
  <>
71
- {[...(tools || []), ...(customTools || [])].map(
72
+ {tools.map(
72
73
  (tool) =>
73
74
  tool && (
74
75
  <StyledAccordion key={tool.id} header={header(tool?.image, tool?.name)} appearance="card" maxHeight={120}>
@@ -54,7 +54,7 @@ const ToolsPanel = () => {
54
54
  tool && (
55
55
  <li key={tool.id}>
56
56
  <ToolBadge
57
- name={tool.name || tool.id}
57
+ name={tool.name || tool.id || ''}
58
58
  image={tool.image ?? ''}
59
59
  description={tool.description ?? ''}
60
60
  />