@stack-spot/ai-chat-widget 2.0.0-betacitric.8 → 2.0.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 (101) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/dist/StackspotAIWidget.d.ts.map +1 -1
  3. package/dist/StackspotAIWidget.js +2 -1
  4. package/dist/StackspotAIWidget.js.map +1 -1
  5. package/dist/app-metadata.json +17 -5
  6. package/dist/chat-interceptors/quick-commands.d.ts.map +1 -1
  7. package/dist/chat-interceptors/quick-commands.js +9 -3
  8. package/dist/chat-interceptors/quick-commands.js.map +1 -1
  9. package/dist/components/HistoryList.d.ts.map +1 -1
  10. package/dist/components/HistoryList.js +2 -0
  11. package/dist/components/HistoryList.js.map +1 -1
  12. package/dist/components/form/DescribedRadioGroup.d.ts.map +1 -1
  13. package/dist/components/form/DescribedRadioGroup.js +1 -1
  14. package/dist/components/form/DescribedRadioGroup.js.map +1 -1
  15. package/dist/hooks/midnight-update-view.d.ts +5 -0
  16. package/dist/hooks/midnight-update-view.d.ts.map +1 -0
  17. package/dist/hooks/midnight-update-view.js +30 -0
  18. package/dist/hooks/midnight-update-view.js.map +1 -0
  19. package/dist/index.d.ts +3 -3
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/layout.css +1 -1
  23. package/dist/state/WidgetState.d.ts +1 -1
  24. package/dist/state/WidgetState.d.ts.map +1 -1
  25. package/dist/state/constants.d.ts.map +1 -1
  26. package/dist/state/constants.js +3 -2
  27. package/dist/state/constants.js.map +1 -1
  28. package/dist/utils/tools.d.ts +9 -2
  29. package/dist/utils/tools.d.ts.map +1 -1
  30. package/dist/utils/tools.js +7 -1
  31. package/dist/utils/tools.js.map +1 -1
  32. package/dist/utils/upload/FileUpload.d.ts.map +1 -1
  33. package/dist/utils/upload/FileUpload.js +1 -2
  34. package/dist/utils/upload/FileUpload.js.map +1 -1
  35. package/dist/views/Agents/AgentDescription.d.ts.map +1 -1
  36. package/dist/views/Agents/AgentDescription.js +21 -13
  37. package/dist/views/Agents/AgentDescription.js.map +1 -1
  38. package/dist/views/Agents/dictionary.d.ts +1 -1
  39. package/dist/views/Agents/dictionary.d.ts.map +1 -1
  40. package/dist/views/Agents/dictionary.js +2 -0
  41. package/dist/views/Agents/dictionary.js.map +1 -1
  42. package/dist/views/Agents/styled.d.ts.map +1 -1
  43. package/dist/views/Agents/styled.js +14 -3
  44. package/dist/views/Agents/styled.js.map +1 -1
  45. package/dist/views/Agents/useAgentFavorites.js +3 -3
  46. package/dist/views/Agents/useAgentFavorites.js.map +1 -1
  47. package/dist/views/Chat/ChatMessage.d.ts.map +1 -1
  48. package/dist/views/Chat/ChatMessage.js +12 -8
  49. package/dist/views/Chat/ChatMessage.js.map +1 -1
  50. package/dist/views/Chat/styled.d.ts.map +1 -1
  51. package/dist/views/Chat/styled.js +24 -0
  52. package/dist/views/Chat/styled.js.map +1 -1
  53. package/dist/views/MessageInput/ContextBar.d.ts.map +1 -1
  54. package/dist/views/MessageInput/ContextBar.js +1 -1
  55. package/dist/views/MessageInput/ContextBar.js.map +1 -1
  56. package/dist/views/MessageInput/SelectContent.d.ts.map +1 -1
  57. package/dist/views/MessageInput/SelectContent.js +6 -0
  58. package/dist/views/MessageInput/SelectContent.js.map +1 -1
  59. package/dist/views/MessageInput/UploadBar.d.ts.map +1 -1
  60. package/dist/views/MessageInput/UploadBar.js +29 -2
  61. package/dist/views/MessageInput/UploadBar.js.map +1 -1
  62. package/dist/views/MessageInput/dictionary.d.ts +1 -1
  63. package/dist/views/MessageInput/dictionary.d.ts.map +1 -1
  64. package/dist/views/MessageInput/dictionary.js +4 -0
  65. package/dist/views/MessageInput/dictionary.js.map +1 -1
  66. package/dist/views/MessageInput/styled.d.ts.map +1 -1
  67. package/dist/views/MessageInput/styled.js +12 -0
  68. package/dist/views/MessageInput/styled.js.map +1 -1
  69. package/dist/views/Resources.d.ts +2 -0
  70. package/dist/views/Resources.d.ts.map +1 -0
  71. package/dist/views/Resources.js +59 -0
  72. package/dist/views/Resources.js.map +1 -0
  73. package/dist/views/Steps/dictionary.d.ts +1 -1
  74. package/dist/views/Tools.js +4 -2
  75. package/dist/views/Tools.js.map +1 -1
  76. package/package.json +13 -10
  77. package/src/StackspotAIWidget.tsx +2 -0
  78. package/src/app-metadata.json +17 -5
  79. package/src/chat-interceptors/quick-commands.ts +11 -5
  80. package/src/components/HistoryList.tsx +3 -0
  81. package/src/components/form/DescribedRadioGroup.tsx +0 -1
  82. package/src/hooks/midnight-update-view.ts +36 -0
  83. package/src/index.ts +4 -3
  84. package/src/layout.css +1 -1
  85. package/src/state/WidgetState.ts +1 -1
  86. package/src/state/constants.ts +3 -2
  87. package/src/utils/tools.ts +15 -3
  88. package/src/utils/upload/FileUpload.ts +1 -2
  89. package/src/views/Agents/AgentDescription.tsx +50 -23
  90. package/src/views/Agents/dictionary.ts +2 -0
  91. package/src/views/Agents/styled.ts +14 -3
  92. package/src/views/Agents/useAgentFavorites.ts +3 -3
  93. package/src/views/Chat/ChatMessage.tsx +41 -7
  94. package/src/views/Chat/styled.ts +24 -0
  95. package/src/views/MessageInput/ContextBar.tsx +1 -2
  96. package/src/views/MessageInput/SelectContent.tsx +6 -0
  97. package/src/views/MessageInput/UploadBar.tsx +40 -2
  98. package/src/views/MessageInput/dictionary.ts +4 -0
  99. package/src/views/MessageInput/styled.ts +12 -0
  100. package/src/views/Resources.tsx +99 -0
  101. package/src/views/Tools.tsx +18 -12
@@ -1,7 +1,7 @@
1
- import { Badge, Skeleton, Text } from '@stack-spot/citric-react'
1
+ import { Icon } from '@stack-spot/citric-icons'
2
+ import { Badge, Card, IconBox, ImageBox, ImageWithFallback, Skeleton, Text } from '@stack-spot/citric-react'
2
3
  import { agentToolsClient } from '@stack-spot/portal-network'
3
4
  import { useMemo } from 'react'
4
- import { ToolBadge } from '../../components/ToolBadge'
5
5
  import { toolById } from '../../utils/tools'
6
6
  import { useAgentsDictionary } from './dictionary'
7
7
  import { AgentDescriptionBox } from './styled'
@@ -10,11 +10,16 @@ export const AgentDescription = ({ agentId }: { agentId?: string }) => {
10
10
  const t = useAgentsDictionary()
11
11
  const [agent, , , { isLoading }] = agentToolsClient.agent.useStatefulQuery({ agentId: agentId! }, { enabled: !!agentId })
12
12
  const [toolKits, , , { isLoading: isLoadingToolKit }] = agentToolsClient.tools.useStatefulQuery({})
13
- const numberOfKnowledgeSources = agent?.knowledge_source_config?.knowledge_sources.length ?? 0
13
+ const numberOfKnowledgeSources = agent?.knowledge_sources_config?.knowledge_sources.length ?? 0
14
14
 
15
15
  const knowledgeSources = useMemo(
16
- () => agent?.knowledge_source_config?.knowledge_sources_details?.map((ks, index) => (
17
- <li key={index}><Badge colorPalette="teal" appearance="square">{ks.name}</Badge></li>
16
+ () => agent?.knowledge_sources_config?.knowledge_sources_details?.map((ks, index) => (
17
+ <li key={index}>
18
+ <Card gap="10px" direction="row" flex={1} size="xxs" bgLevel={500} justifyContent="space-between">
19
+ <Text color="light.contrastText">{ks.name}</Text>
20
+ <Badge colorScheme="inverse" appearance="square">{ks.type}</Badge>
21
+ </Card>
22
+ </li>
18
23
  )),
19
24
  [agent],
20
25
  )
@@ -25,32 +30,50 @@ export const AgentDescription = ({ agentId }: { agentId?: string }) => {
25
30
  }
26
31
  return loadingKS
27
32
  }, [numberOfKnowledgeSources])
28
- const tools = useMemo(() => {
29
- const result: React.ReactElement[] = []
30
- const builtInTools = agent?.toolkits?.builtin_toolkits?.[0]?.tools
33
+
34
+ const { tools, multiAgents } = useMemo(() => {
35
+ const tools: React.ReactElement[] = []
36
+ const multiAgents: React.ReactElement[] = []
37
+ const builtInTools = agent?.toolkits?.builtin_toolkits ?? []
31
38
  const customToolkits = agent?.toolkits?.custom_toolkits ?? []
32
- for (const tool of builtInTools ?? []) {
33
- const toolWithImage = toolById(tool.id, toolKits)
34
- result.push(
35
- <li key={tool.id}>
36
- <ToolBadge
37
- name={toolWithImage?.name || toolWithImage?.id || 'unknown'}
38
- image={toolWithImage?.image}
39
- style={{ maxWidth: '150px' }}
40
- />
41
- </li>,
42
- )
39
+ for (const toolkit of builtInTools) {
40
+ for (const tool of toolkit.tools ?? []) {
41
+ if (toolkit.id == 'UTILITIES'){
42
+ const toolWithImage = toolById(tool.id, toolKits)
43
+ tools.push(
44
+ <li key={tool.id}>
45
+ <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>
48
+ </Card>
49
+ </li>,
50
+ )
51
+ }
52
+ if (toolkit.id == 'MULTI_AGENTS'){
53
+ multiAgents.push(
54
+ <li key={tool.id}>
55
+ <Card gap="10px" direction="row" flex={1} size="xxs" bgLevel={500}>
56
+ <IconBox icon="Agent" />
57
+ <Text color="light.contrastText">{tool.name}</Text>
58
+ </Card>
59
+ </li>,
60
+ )
61
+ }
62
+ }
43
63
  }
44
64
  for (const toolkit of customToolkits) {
45
65
  for (const tool of toolkit.tools) {
46
- result.push(
66
+ tools.push(
47
67
  <li key={tool.id}>
48
- <ToolBadge name={tool.name} image={toolkit.avatar ?? undefined} style={{ maxWidth: '150px' }} />
68
+ <Card gap="10px" direction="row" flex={1} size="xxs" bgLevel={500}>
69
+ <ImageBox><ImageWithFallback src={toolkit.avatar ?? undefined} fallback={<Icon icon="Cog" />} /></ImageBox>
70
+ <Text color="light.contrastText">{tool.name}</Text>
71
+ </Card>
49
72
  </li>,
50
73
  )
51
74
  }
52
75
  }
53
- return result
76
+ return { tools, multiAgents }
54
77
  }, [agent])
55
78
 
56
79
  return (
@@ -63,10 +86,14 @@ export const AgentDescription = ({ agentId }: { agentId?: string }) => {
63
86
  <Text appearance="microtext1" className="title">Knowledge sources</Text>
64
87
  <ul>{isLoading || isLoadingToolKit ? skeleton : knowledgeSources}</ul>
65
88
  </section>}
66
- {!!tools.length && <section>
89
+ {!!tools?.length && <section>
67
90
  <Text appearance="microtext1" className="title">{t.tools}</Text>
68
91
  <ul>{tools}</ul>
69
92
  </section>}
93
+ {!!multiAgents?.length && <section>
94
+ <Text appearance="microtext1" className="title">{t.multiAgent}</Text>
95
+ <ul>{multiAgents}</ul>
96
+ </section>}
70
97
  {agent?.model_name && <section>
71
98
  <Text appearance="microtext1" className="title">LLM</Text>
72
99
  <Badge colorPalette="orange" appearance="square">{agent?.model_name}</Badge>
@@ -18,6 +18,7 @@ const dictionary = {
18
18
  favorites: 'Favorites',
19
19
  tools: 'Tools',
20
20
  spots: 'Spots',
21
+ multiAgent: 'Multi-agents',
21
22
  },
22
23
  pt: {
23
24
  title: 'Agentes',
@@ -36,6 +37,7 @@ const dictionary = {
36
37
  favorites: 'Favoritos',
37
38
  tools: 'Ferramentas',
38
39
  spots: 'Spots',
40
+ multiAgent: 'Multi-agents',
39
41
 
40
42
  },
41
43
  } satisfies Dictionary
@@ -18,9 +18,11 @@ export const AgentLabel = styled.div`
18
18
  export const AgentDescriptionBox = styled.div`
19
19
  color: ${theme.color.light[700]};
20
20
  line-height: 18px;
21
+ background-color: ${theme.color.light[400]};
22
+ margin: -10px;
23
+ padding: 8px;
21
24
 
22
25
  section {
23
- border-bottom: 1px solid ${theme.color.light[600]};
24
26
  padding-bottom: 10px;
25
27
  margin-bottom: 10px;
26
28
 
@@ -34,6 +36,14 @@ export const AgentDescriptionBox = styled.div`
34
36
  display: block;
35
37
  margin-bottom: 6px;
36
38
  }
39
+
40
+ .card-agent-info {
41
+ background-color: ${theme.color.light[500]};
42
+ border: 1px solid ${theme.color.light[600]};
43
+ border-radius: 3px;
44
+ padding: 4px;
45
+ width: 100%;
46
+ }
37
47
  }
38
48
 
39
49
  ul {
@@ -44,10 +54,11 @@ export const AgentDescriptionBox = styled.div`
44
54
  flex-direction: row;
45
55
  flex-wrap: wrap;
46
56
  white-space: nowrap;
47
- gap: 6px;
48
57
 
49
58
  li {
50
- margin: 3px 0;
59
+ margin-bottom: 3px;
60
+ display: flex;
61
+ width: 100%;
51
62
  }
52
63
  }
53
64
 
@@ -1,10 +1,10 @@
1
1
  /* eslint-disable filenames/match-regex */
2
- import { agentClient, agentToolsClient } from '@stack-spot/portal-network'
2
+ import { agentToolsClient } from '@stack-spot/portal-network'
3
3
 
4
4
  export function useAgentFavorites() {
5
5
  const useFavorites = () => agentToolsClient.agents.useQuery({ visibility: 'favorite' })
6
- const [addFavorite, pendingAddFav] = agentClient.addFavoriteAgent.useMutation()
7
- const [removeFavorite, pendingRemoveFav] = agentClient.removeFavoriteAgent.useMutation()
6
+ const [addFavorite, pendingAddFav] = agentToolsClient.addFavorite.useMutation()
7
+ const [removeFavorite, pendingRemoveFav] = agentToolsClient.removeFavorite.useMutation()
8
8
 
9
9
  const removeFavoriteAgent = async (idOrSlug?: string) => {
10
10
  try {
@@ -1,5 +1,5 @@
1
1
  import { Icon } from '@stack-spot/citric-icons'
2
- import { Alert, Badge, Button, CheckboxGroup, Column, IconButton, Input, RadioGroup, Row, Text, Tooltip } from '@stack-spot/citric-react'
2
+ import { Alert, Badge, Button, CheckboxGroup, Column, IconButton, ImageBox, ImageWithFallback, Input, RadioGroup, Row, Text, Tooltip } from '@stack-spot/citric-react'
3
3
  import { agentToolsClient } from '@stack-spot/portal-network'
4
4
  import { listToClass, theme } from '@stack-spot/portal-theme'
5
5
  import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
@@ -9,8 +9,8 @@ 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 { StackedBadge } from '../../components/StackedBadge'
13
12
  import { useChatEntry, useCurrentChat, useWidget } from '../../context/hooks'
13
+ import { useMidnightUpdateView } from '../../hooks/midnight-update-view'
14
14
  import { ChatEntry, SerializableAction, TextChatEntry } from '../../state/ChatEntry'
15
15
  import { useDateFormatter } from '../../utils/date'
16
16
  import { toolById } from '../../utils/tools'
@@ -214,10 +214,13 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
214
214
  const chat = useCurrentChat()
215
215
  const agentId = entry.agent?.id ?? ''
216
216
  const [toolKits] = agentToolsClient.tools.useStatefulQuery({}, { enabled: !!agentId })
217
+ const [agentsTools] = agentToolsClient.agentsByIds.useStatefulQuery(
218
+ { searchAgentsRequest: { ids: entry.tools || [''] } }, { enabled: !!entry.tools })
217
219
  const [copied, setCopied] = useState(false)
218
220
  const [showUserButtonCopy, setShowUserButtonCopy] = useState(false)
219
221
 
220
222
  useChatScrollToBottomEffect(ref, [entry])
223
+ useMidnightUpdateView()
221
224
 
222
225
  const detailKS = useCallback(({ name, slug, documentScore, documentId }: Required<TextChatEntry>['knowledgeSources'][number]) => {
223
226
  widget.set('currentKSInPanel', { name, slug, score: documentScore, documentId })
@@ -335,12 +338,11 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
335
338
  </form>
336
339
  }
337
340
 
338
- function openToolsPanel() {
341
+ function openResourcesPanel() {
339
342
  widget.set('currentMessageInPanel', { chatId: chat.id, messageId: message.id })
340
- widget.set('panel', 'tools')
343
+ widget.set('panel', 'resources')
341
344
  }
342
345
 
343
-
344
346
  return (entry.content || entry.error || !!entry.steps?.length || entry.upload?.length) && (
345
347
  <li key={entry.messageId} className={entry.agentType} ref={ref}>
346
348
  <div className="chat-message-container"
@@ -363,7 +365,7 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
363
365
  {entry.error && <Alert type="error">{entry.error}</Alert>}
364
366
  </div>
365
367
  {afterMessage && createElement(afterMessage, { message })}
366
- {!!entry.tools?.length && <StackedBadge
368
+ {/* {!!entry.tools?.length && <StackedBadge
367
369
  aria-label={t.openToolsPanel}
368
370
  title={t.openToolsPanel}
369
371
  tabIndex={0}
@@ -376,7 +378,8 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
376
378
  })}
377
379
  onClick={openToolsPanel}
378
380
  style={{ marginTop: '12px', width: 'fit-content' }}
379
- />}
381
+ />} */}
382
+
380
383
  {!!entry.knowledgeSources?.length && <div className="ks-box">
381
384
  <Text appearance="microtext1" color="light.700">Knowledge Sources:</Text>
382
385
  <ul>{entry.knowledgeSources.map((ks, index) => (
@@ -385,6 +388,35 @@ export const ChatMessage = ({ message, isLast, beforeMessage, afterMessage, cust
385
388
  </li>
386
389
  ))}</ul>
387
390
  </div>}
391
+
392
+ {(!!agentsTools?.length || !!entry.tools?.length) && <div className="tools-box">
393
+ <Button appearance="none" onClick={openResourcesPanel} aria-label={t.openResourcesPanel}>
394
+ {agentsTools?.map((agent) => (
395
+ <ImageBox key={agent.id} className="agent-info-avatar-resource">
396
+ <ImageWithFallback
397
+ src={agent.avatar ?? undefined}
398
+ fallback={<Icon icon="Agent" />}
399
+ alt={agent.name}
400
+ aria-label={agent.name}
401
+ title={agent.name}
402
+ />
403
+ </ImageBox>
404
+ ))}
405
+ {entry.tools?.map((id) => {
406
+ const tool = toolById(id, toolKits)
407
+ return (
408
+ <ImageBox key={id} className="agent-info-avatar-resource">
409
+ <ImageWithFallback
410
+ src={tool?.image}
411
+ fallback={<Icon icon="Cog" />}
412
+ alt={tool?.name}
413
+ aria-label={tool?.name}
414
+ title={tool?.name}
415
+ />
416
+ </ImageBox>
417
+ )})}
418
+ </Button>
419
+ </div>}
388
420
 
389
421
  {shouldShowFooter && <div className="message-footer">
390
422
  {entry.agentType === 'bot' && !entry.error && <div className="message-actions">
@@ -467,6 +499,7 @@ const dictionary = {
467
499
  dislike: 'Dislike',
468
500
  tools: 'Tools',
469
501
  openToolsPanel: 'Open the tools panel to see more details.',
502
+ openResourcesPanel: 'Open the resources panel to see more details.',
470
503
  copied: 'Copied',
471
504
  },
472
505
  pt: {
@@ -475,6 +508,7 @@ const dictionary = {
475
508
  dislike: 'Não gostei',
476
509
  tools: 'Ferramentas',
477
510
  openToolsPanel: 'Abrir o painel de ferramentas para ver mais detalhes.',
511
+ openResourcesPanel: 'Abrir o painel de recursos para ver mais detalhes.',
478
512
  copied: 'Copiado',
479
513
  },
480
514
  } satisfies Dictionary
@@ -218,6 +218,30 @@ export const ChatList: IStyledComponentBase<
218
218
  }
219
219
  }
220
220
 
221
+ .tools-box {
222
+
223
+ > ul {
224
+ display: flex;
225
+ flex-direction: row;
226
+ flex-wrap: wrap;
227
+ white-space: nowrap;
228
+ margin: 0;
229
+ margin-top: 8px;
230
+ padding: 0;
231
+ list-style: none;
232
+ gap: 6px;
233
+
234
+ &:hover{
235
+ cursor: pointer;
236
+
237
+ .agent-info-avatar-resource {
238
+ transition: margin-left 0.3s ease-in;
239
+ margin-left: 0px;
240
+ }
241
+ }
242
+ }
243
+ }
244
+
221
245
  .steps {
222
246
  ul {
223
247
  list-style: none;
@@ -22,8 +22,7 @@ const ContextBadge = ({ label, color, dismiss, onDismiss }: ContextBadgeProps) =
22
22
  onClick={onDismiss}
23
23
  title={dismiss}
24
24
  arial-label={dismiss}
25
- style={{ padding: '2px 0 2px 2px' }}
26
- colorPalette={color}
25
+ style={{ padding: '2px 0 2px 2px', color: 'inherit' }}
27
26
  />}
28
27
  </Badge>
29
28
  )
@@ -15,6 +15,7 @@ export const SelectContent = () => {
15
15
 
16
16
  const itemConfigs = useMemo<(MenuAction & { key: keyof typeof features, section: 0 | 1 })[]>(() => ([
17
17
  {
18
+ role: 'button',
18
19
  key: 'agent',
19
20
  section: 0,
20
21
  label: t.chatAgent,
@@ -25,6 +26,7 @@ export const SelectContent = () => {
25
26
  },
26
27
  },
27
28
  {
29
+ role: 'button',
28
30
  key: 'quickCommands',
29
31
  section: 0,
30
32
  label: 'Quick Commands',
@@ -35,6 +37,7 @@ export const SelectContent = () => {
35
37
  },
36
38
  },
37
39
  {
40
+ role: 'button',
38
41
  key: 'knowledgeSource',
39
42
  section: 0,
40
43
  label: 'Knowledge Sources',
@@ -42,6 +45,7 @@ export const SelectContent = () => {
42
45
  onClick: () => widget.set('panel', 'ks'),
43
46
  },
44
47
  {
48
+ role: 'button',
45
49
  key: 'stack',
46
50
  section: 0,
47
51
  label: 'Stacks AI',
@@ -49,6 +53,7 @@ export const SelectContent = () => {
49
53
  onClick: () => widget.set('panel', 'stack'),
50
54
  },
51
55
  {
56
+ role: 'button',
52
57
  key: 'workspace',
53
58
  section: 0,
54
59
  label: 'Spots',
@@ -56,6 +61,7 @@ export const SelectContent = () => {
56
61
  onClick: () => widget.set('panel', 'workspace'),
57
62
  },
58
63
  {
64
+ role: 'button',
59
65
  key: 'upload',
60
66
  section: 1,
61
67
  label: t.upload,
@@ -1,6 +1,6 @@
1
1
  import { listToClass } from '@stack-spot/portal-theme'
2
2
  import { interpolate } from '@stack-spot/portal-translate'
3
- import { useMemo } from 'react'
3
+ import { useEffect, useMemo, useRef, useState } from 'react'
4
4
  import { FadingOverflow } from '../../components/FadingOverflow'
5
5
  import { FileDescription } from '../../components/FileDescription'
6
6
  import { useCurrentChat } from '../../context/hooks'
@@ -22,6 +22,7 @@ const UploadItem = ({ upload }: UploadedFileProps) => {
22
22
  const uploadManager = useUploadManager()
23
23
  const icon = upload.file.type.toLowerCase().startsWith('image/') ? createImageFromFile(upload.file) : undefined
24
24
  const status = useUploadStatus(upload)
25
+
25
26
  return <FileDescription
26
27
  fileName={upload.file.name}
27
28
  icon={icon}
@@ -37,6 +38,29 @@ export const UploadBar = () => {
37
38
  const t = useMessageInputDictionary()
38
39
  const chat = useCurrentChat()
39
40
  const visible = !!uploads.length
41
+ const [ariaMessage, setAriaMessage] = useState('')
42
+ const prevUploadsRef = useRef<FileUpload[]>([])
43
+ const announcedUploadsRef = useRef(new Set())
44
+
45
+ useEffect(() => {
46
+ const prevUploads = prevUploadsRef.current
47
+ const newUploads = uploads.filter(
48
+ up => !prevUploads.some(prev => prev.id === up.id),
49
+ )
50
+ newUploads.forEach((up) => {
51
+ if (!announcedUploadsRef.current.has(up.id)) {
52
+ setAriaMessage('')
53
+ setTimeout(() => {
54
+ setAriaMessage(`${t.uploadSuccessStatus}: ${up.file.name}`)
55
+ setTimeout(() => setAriaMessage(''), 2500)
56
+ }, 100)
57
+ announcedUploadsRef.current.add(up.id)
58
+ }
59
+ })
60
+ if (uploads.some(up => up.status === 'error')) {
61
+ setTimeout(() => setAriaMessage(''), 2000)
62
+ }
63
+ }, [uploads])
40
64
 
41
65
  useUploadErrorEffect((errors) => {
42
66
  const sizeErrors = errors.filter(e => e instanceof FileIsTooLarge)
@@ -53,11 +77,25 @@ export const UploadBar = () => {
53
77
  lines.push(`${interpolate(t.uploadItemLimitError, maxItems)}\n- ${maxItemsErrorsNames.join('\n- ')}`)
54
78
  }
55
79
  if (!lines.length) return
80
+ setAriaMessage(lines.join(' '))
81
+ setTimeout(() => setAriaMessage(''), 2000)
56
82
  chat.pushMessage(new ChatEntry({ agentType: 'system', type: 'md', content: lines.join('\n\n') }))
57
- })
58
83
 
84
+ if (lines.length) {
85
+ setAriaMessage(lines.join(' '))
86
+ setTimeout(() => setAriaMessage(''), 2000)
87
+ chat.pushMessage(new ChatEntry({ agentType: 'system', type: 'md', content: lines.join('\n\n') }))
88
+ }
89
+ })
90
+
59
91
  return (
60
92
  <div className={listToClass(['info-bar', 'upload-bar', visible && 'visible'])}>
93
+ <div className="aria-live"
94
+ aria-live="polite"
95
+ aria-atomic="true"
96
+ >
97
+ {ariaMessage}
98
+ </div>
61
99
  <div className="space"></div>
62
100
  <div className="content">
63
101
  <FadingOverflow className="list-overflow" scroll="arrows" enableHorizontalScrollWithVerticalWheel>
@@ -33,6 +33,8 @@ const dictionary = {
33
33
  cantSendBecauseOfEmptyContent: 'You can\'t send empty messages. Please write some text or upload a file.',
34
34
  cantSendBecausePromptMaxLength: 'You can\'t send messages longer than $0 characters. Please, shorten your message.',
35
35
  chatAgent: 'Agents',
36
+ uploadSuccessStatus: 'File sent successfully',
37
+ chatViewMenu: 'Chat options menu',
36
38
  },
37
39
  pt: {
38
40
  stack: 'Selecionar stack',
@@ -66,6 +68,8 @@ const dictionary = {
66
68
  cantSendBecauseOfEmptyContent: 'Não é possível enviar mensagens vazias. Por favor, escreva algum texto ou envie um arquivo.',
67
69
  cantSendBecausePromptMaxLength: 'Você não pode enviar mensagens com mais de $0 caracteres. Por favor, reduza sua mensagem.',
68
70
  chatAgent: 'Agentes',
71
+ uploadSuccessStatus: 'Arquivo anexado com sucesso',
72
+ chatViewMenu: 'Menu de opções do chat',
69
73
  },
70
74
  } satisfies Dictionary
71
75
 
@@ -64,6 +64,18 @@ export const MessageInputBox = styled.div`
64
64
  }
65
65
  }
66
66
 
67
+ > .aria-live {
68
+ position: absolute;
69
+ width: 1px;
70
+ height: 1px;
71
+ margin: -1px;
72
+ padding: 0;
73
+ overflow: hidden;
74
+ clip: rect(0 0 0 0);
75
+ white-space: nowrap;
76
+ border: 0;
77
+ }
78
+
67
79
  > .space {
68
80
  height: 0;
69
81
  transition: height 0.4s;
@@ -0,0 +1,99 @@
1
+ import { Accordion, Icon, ImageBox, ImageWithFallback, Row, Text } from '@stack-spot/citric-react'
2
+ import { agentToolsClient } from '@stack-spot/portal-network'
3
+ import { theme } from '@stack-spot/portal-theme'
4
+ import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
5
+ import { useEffect, useMemo } from 'react'
6
+ import { styled } from 'styled-components'
7
+ import { useWidget, useWidgetState } from '../context/hooks'
8
+ import { useRightPanel } from '../right-panel/hooks'
9
+ import { toolById } from '../utils/tools'
10
+ import { AgentDescription } from './Agents/AgentDescription'
11
+
12
+ export const Resources = () => {
13
+ const t = useTranslate(dictionary)
14
+ const panel = useWidgetState('panel')
15
+ const message = useWidgetState('currentMessageInPanel')
16
+ const { open } = useRightPanel()
17
+ const widget = useWidget()
18
+
19
+ useEffect(() => {
20
+ if (panel === 'resources' && message) open(
21
+ <ResourcesPanel key={message.messageId} />,
22
+ { title: t.title, description: t.description, onClose: () => widget.set('panel', undefined) },
23
+ )
24
+ }, [panel, t])
25
+
26
+ return null
27
+ }
28
+
29
+ const StyledAccordion = styled(Accordion)`
30
+ &[data-citric="accordion"] {
31
+ background-color: ${theme.color.light[400]};
32
+ }
33
+ `
34
+
35
+ const ResourcesPanel = () => {
36
+ const { chatId, messageId } = useWidgetState('currentMessageInPanel') ?? {}
37
+ const widget = useWidget()
38
+ const message = useMemo(() => {
39
+ const chat = widget.chatTabs.getAll().find(c => c.id === chatId)
40
+ return chat?.getMessages().find(m => m.id === messageId)?.getValue()
41
+ }, [messageId])
42
+
43
+ const [toolKits] = agentToolsClient.tools.useStatefulQuery({}, { enabled: !!message?.agent?.id })
44
+ const [agent] = agentToolsClient.agent.useStatefulQuery({ agentId: message?.agent?.id || '' },
45
+ { enabled: !!message?.agent?.id })
46
+ const tools = useMemo(() => message?.tools?.map(id => toolById(id, toolKits)), [messageId, toolKits])
47
+ const customTools = useMemo(() => message?.tools?.map(id => toolById(id, agent?.toolkits?.custom_toolkits)),
48
+ [messageId, agent?.toolkits?.custom_toolkits])
49
+
50
+ const [agentsTools] = agentToolsClient.agentsByIds.useStatefulQuery({ searchAgentsRequest:{ ids: message?.tools || [] } })
51
+ const hasAgentTool = useMemo(() => message?.tools?.some(id => agentsTools?.find((agent) => agent.id === id)), [messageId, toolKits])
52
+
53
+ const header = (image?: string, label?: string) => (
54
+ <Row gap="10px">
55
+ <ImageBox>
56
+ <ImageWithFallback src={image} fallback={<Icon icon="Agent" />} aria-label={label} title={label} />
57
+ </ImageBox>
58
+ <Text>{label}</Text>
59
+ </Row>
60
+ )
61
+
62
+ return !!(tools?.length || customTools?.length) && (
63
+ <>
64
+ <>
65
+ {[...(tools || []), ...(customTools || [])].map(
66
+ (tool) =>
67
+ tool && (
68
+ <StyledAccordion key={tool.id} header={header(tool?.image, tool?.name)} appearance="card">
69
+ {tool?.description}
70
+ </StyledAccordion>
71
+ ))}
72
+ </>
73
+ {
74
+ hasAgentTool &&
75
+ <>
76
+ {message?.tools?.map((id) => {
77
+ const agentTool = agentsTools?.find((agent) => agent.id === id)
78
+ return (
79
+ <StyledAccordion key={id} header={header(agentTool?.avatar || undefined, agentTool?.name)}>
80
+ <AgentDescription agentId={id} />
81
+ </StyledAccordion>
82
+ )},
83
+ )}
84
+ </>
85
+ }
86
+ </>
87
+ )
88
+ }
89
+
90
+ const dictionary = {
91
+ en: {
92
+ title: 'Resources',
93
+ description: 'These are the resources used to generate this answer.',
94
+ },
95
+ pt: {
96
+ title: 'Recursos',
97
+ description: 'Esses são os recursos usados pra gerar essa resposta.',
98
+ },
99
+ } satisfies Dictionary
@@ -42,19 +42,25 @@ const ToolsPanel = () => {
42
42
  }, [messageId])
43
43
 
44
44
  const [toolKits] = agentToolsClient.tools.useStatefulQuery({}, { enabled: !!message?.agent?.id })
45
- const tools = useMemo(() => message?.tools?.map(id => toolById(id, toolKits) ?? { id }), [messageId, toolKits])
46
-
47
- return !!tools?.length && (
45
+ const [agent] = agentToolsClient.agent.useStatefulQuery({ agentId: message?.agent?.id || '' },
46
+ { enabled: !!message?.agent?.id })
47
+ const tools = useMemo(() => message?.tools?.map(id => toolById(id, toolKits)), [messageId, toolKits])
48
+ const customTools = useMemo(() => message?.tools?.map(id => toolById(id, agent?.toolkits?.custom_toolkits)),
49
+ [messageId, agent?.toolkits?.custom_toolkits])
50
+ return !!(tools?.length || customTools?.length) && (
48
51
  <ToolList>
49
- {tools.map((tool) => (
50
- <li key={tool.id}>
51
- <ToolBadge
52
- name={tool.name || tool.id}
53
- image={tool.image}
54
- description={tool.description}
55
- />
56
- </li>
57
- ))}
52
+ {[...(tools || []), ...(customTools || [])].map(
53
+ (tool) =>
54
+ tool && (
55
+ <li key={tool.id}>
56
+ <ToolBadge
57
+ name={tool.name || tool.id}
58
+ image={tool.image ?? ''}
59
+ description={tool.description ?? ''}
60
+ />
61
+ </li>
62
+ ),
63
+ )}
58
64
  </ToolList>
59
65
  )
60
66
  }