@stack-spot/ai-chat-widget 1.6.0 → 1.7.0-beta.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 (61) hide show
  1. package/dist/app-metadata.json +7 -7
  2. package/dist/components/Selector/index.d.ts +20 -0
  3. package/dist/components/Selector/index.d.ts.map +1 -0
  4. package/dist/components/Selector/index.js +134 -0
  5. package/dist/components/Selector/index.js.map +1 -0
  6. package/dist/components/Selector/styled.d.ts +2 -0
  7. package/dist/components/Selector/styled.d.ts.map +1 -0
  8. package/dist/components/Selector/styled.js +144 -0
  9. package/dist/components/Selector/styled.js.map +1 -0
  10. package/dist/regex.d.ts +1 -0
  11. package/dist/regex.d.ts.map +1 -1
  12. package/dist/regex.js +1 -0
  13. package/dist/regex.js.map +1 -1
  14. package/dist/views/Agents/AgentsTab.js +4 -4
  15. package/dist/views/Agents/AgentsTab.js.map +1 -1
  16. package/dist/views/Chat/AgentInfo.js +1 -1
  17. package/dist/views/Home/CustomAgent.js +3 -3
  18. package/dist/views/Home/CustomAgent.js.map +1 -1
  19. package/dist/views/Home/styled.js +1 -1
  20. package/dist/views/MessageInput/AgentSelector.d.ts +4 -0
  21. package/dist/views/MessageInput/AgentSelector.d.ts.map +1 -0
  22. package/dist/views/MessageInput/AgentSelector.js +31 -0
  23. package/dist/views/MessageInput/AgentSelector.js.map +1 -0
  24. package/dist/views/MessageInput/ButtonAgent.d.ts +2 -0
  25. package/dist/views/MessageInput/ButtonAgent.d.ts.map +1 -0
  26. package/dist/views/MessageInput/ButtonAgent.js +17 -0
  27. package/dist/views/MessageInput/ButtonAgent.js.map +1 -0
  28. package/dist/views/MessageInput/ButtonGroup.d.ts +1 -1
  29. package/dist/views/MessageInput/ButtonGroup.d.ts.map +1 -1
  30. package/dist/views/MessageInput/ButtonGroup.js +4 -6
  31. package/dist/views/MessageInput/ButtonGroup.js.map +1 -1
  32. package/dist/views/MessageInput/QuickCommandSelector.d.ts +2 -11
  33. package/dist/views/MessageInput/QuickCommandSelector.d.ts.map +1 -1
  34. package/dist/views/MessageInput/QuickCommandSelector.js +17 -130
  35. package/dist/views/MessageInput/QuickCommandSelector.js.map +1 -1
  36. package/dist/views/MessageInput/dictionary.d.ts +1 -1
  37. package/dist/views/MessageInput/dictionary.d.ts.map +1 -1
  38. package/dist/views/MessageInput/dictionary.js +4 -2
  39. package/dist/views/MessageInput/dictionary.js.map +1 -1
  40. package/dist/views/MessageInput/index.d.ts.map +1 -1
  41. package/dist/views/MessageInput/index.js +4 -1
  42. package/dist/views/MessageInput/index.js.map +1 -1
  43. package/dist/views/MessageInput/styled.d.ts.map +1 -1
  44. package/dist/views/MessageInput/styled.js +51 -144
  45. package/dist/views/MessageInput/styled.js.map +1 -1
  46. package/package.json +1 -1
  47. package/src/app-metadata.json +7 -7
  48. package/src/components/Selector/index.tsx +245 -0
  49. package/src/components/Selector/styled.ts +145 -0
  50. package/src/regex.ts +1 -0
  51. package/src/views/Agents/AgentsTab.tsx +4 -4
  52. package/src/views/Chat/AgentInfo.tsx +1 -1
  53. package/src/views/Home/CustomAgent.tsx +3 -3
  54. package/src/views/Home/styled.ts +1 -1
  55. package/src/views/MessageInput/AgentSelector.tsx +35 -0
  56. package/src/views/MessageInput/ButtonAgent.tsx +36 -0
  57. package/src/views/MessageInput/ButtonGroup.tsx +3 -10
  58. package/src/views/MessageInput/QuickCommandSelector.tsx +21 -205
  59. package/src/views/MessageInput/dictionary.ts +4 -2
  60. package/src/views/MessageInput/index.tsx +8 -3
  61. package/src/views/MessageInput/styled.ts +51 -144
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@stack-spot/ai-chat-widget",
3
- "version": "1.6.0",
4
- "date": "Tue Jan 14 2025 23:31:13 GMT+0000 (Coordinated Universal Time)",
3
+ "version": "1.7.0-beta.0",
4
+ "date": "Tue Jan 21 2025 08:16:05 GMT-0300 (Brasilia Standard Time)",
5
5
  "dependencies": [
6
6
  {
7
7
  "name": "@stack-spot/app-metadata",
@@ -89,15 +89,15 @@
89
89
  },
90
90
  {
91
91
  "name": "@citric/core",
92
- "version": "6.2.0(lodash@4.17.21)(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0))"
92
+ "version": "6.4.0(lodash@4.17.21)(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0))"
93
93
  },
94
94
  {
95
95
  "name": "@citric/icons",
96
- "version": "5.7.7(react@18.2.0)"
96
+ "version": "5.9.0(react@18.2.0)"
97
97
  },
98
98
  {
99
99
  "name": "@citric/ui",
100
- "version": "6.1.2(@citric/core@6.2.0(lodash@4.17.21)(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0)))(@citric/icons@5.7.7(react@18.2.0))(lodash@4.17.21)(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0))"
100
+ "version": "6.5.5(@citric/core@6.4.0(lodash@4.17.21)(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0)))(@citric/icons@5.9.0(react@18.2.0))(lodash@4.17.21)(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0))"
101
101
  },
102
102
  {
103
103
  "name": "@monaco-editor/react",
@@ -105,7 +105,7 @@
105
105
  },
106
106
  {
107
107
  "name": "@stack-spot/portal-components",
108
- "version": "2.8.1(@citric/core@6.2.0(lodash@4.17.21)(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0)))(@citric/icons@5.7.7(react@18.2.0))(@citric/ui@6.1.2(@citric/core@6.2.0(lodash@4.17.21)(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0)))(@citric/icons@5.7.7(react@18.2.0))(lodash@4.17.21)(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0)))(@stack-spot/portal-theme@1.1.0(@citric/core@6.2.0(lodash@4.17.21)(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0)))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0)))(@stack-spot/portal-translate@1.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.3.11)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)"
108
+ "version": "2.8.1(@citric/core@6.4.0(lodash@4.17.21)(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0)))(@citric/icons@5.9.0(react@18.2.0))(@citric/ui@6.5.5(@citric/core@6.4.0(lodash@4.17.21)(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0)))(@citric/icons@5.9.0(react@18.2.0))(lodash@4.17.21)(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0)))(@stack-spot/portal-theme@1.1.0(@citric/core@6.4.0(lodash@4.17.21)(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0)))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0)))(@stack-spot/portal-translate@1.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(@types/react@18.3.11)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)"
109
109
  },
110
110
  {
111
111
  "name": "@stack-spot/portal-network",
@@ -113,7 +113,7 @@
113
113
  },
114
114
  {
115
115
  "name": "@stack-spot/portal-theme",
116
- "version": "1.1.0(@citric/core@6.2.0(lodash@4.17.21)(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0)))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0))"
116
+ "version": "1.1.0(@citric/core@6.4.0(lodash@4.17.21)(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0)))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(styled-components@6.1.10(react-dom@18.2.0(react@18.2.0))(react@18.2.0))"
117
117
  },
118
118
  {
119
119
  "name": "@stack-spot/portal-translate",
@@ -0,0 +1,245 @@
1
+ import { IconBox, Image, Text } from '@citric/core'
2
+ import { Agent, ExternalLink } from '@citric/icons'
3
+ import { IconButton } from '@citric/ui'
4
+ import { useKeyboardControls } from '@stack-spot/portal-components'
5
+ import { VisibilityLevelEnum } from '@stack-spot/portal-network/api/ai'
6
+ import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
7
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
8
+ import { useCurrentChatState } from '../../context/hooks'
9
+ import { getUrlToStackSpotAI } from '../../utils/url'
10
+ import { Fading } from '../Fading'
11
+ import { FallbackBoundary } from '../FallbackBoundary'
12
+ import { SelectorBox } from './styled'
13
+
14
+ const sections = [undefined, 'personal', 'workspace', 'account', 'shared'] as const
15
+
16
+ type SelectorShortcut = '/' | '@'
17
+
18
+ interface Item {
19
+ id: string,
20
+ slug: string,
21
+ description: string,
22
+ visibility_level: VisibilityLevelEnum,
23
+ }
24
+
25
+ interface ListProps<T> {
26
+ filter?: string,
27
+ visibility?: VisibilityLevelEnum,
28
+ selectorConfig: SelectorConfig<T>,
29
+ onSelect: (item: T) => void,
30
+ }
31
+
32
+ interface ListItemProps<T> extends ListProps<T> {
33
+ item: T,
34
+ }
35
+
36
+ interface ContentProps<T> {
37
+ filter?: string,
38
+ onClose: () => void,
39
+ inputRef: React.RefObject<HTMLTextAreaElement | HTMLInputElement>,
40
+ selectorConfig: SelectorConfig<T>,
41
+ }
42
+
43
+ interface SelectorConfig<T> {
44
+ resourceName: string,
45
+ shortcut: SelectorShortcut,
46
+ icon: React.ReactElement,
47
+ regex: RegExp,
48
+ url: string,
49
+ imageProp?: keyof T,
50
+ data: () => T[],
51
+ isEnabled: boolean,
52
+ onSelect: (item: T) => void,
53
+ }
54
+
55
+ interface SelectorProps<T> {
56
+ selectorConfig: SelectorConfig<T>,
57
+ inputRef: React.RefObject<HTMLTextAreaElement | HTMLInputElement>,
58
+ }
59
+
60
+ const ListItem = <T extends Item>({ item, selectorConfig, onSelect }: ListItemProps<T>) => {
61
+ const t = useTranslate(dictionary)
62
+ const { shortcut, url } = selectorConfig
63
+ const hasImage = !!selectorConfig.imageProp
64
+ const image = item[selectorConfig.imageProp!] as string
65
+ const linkTitle = t.open.replace('%s', item.slug)
66
+
67
+ return (
68
+ <li>
69
+ {!!hasImage && image ? <Image width="32" height="32" radius="full" src={image} /> : <IconBox size="md"><Agent /></IconBox>}
70
+ <button
71
+ className="selector"
72
+ onClick={() => onSelect(item)}
73
+ onKeyDown={(e) => e.key === 'Enter' && e.preventDefault()}
74
+ onFocus={(e) => e.target.closest('li')?.classList.add('focus')}
75
+ onBlur={(e) => e.target.closest('li')?.classList.remove('focus')}
76
+ >
77
+ <p className="selector-title">{!hasImage ? shortcut : ''}{item.slug}</p>
78
+ <p className="selector-description">{item.description}</p>
79
+ </button>
80
+ <IconButton as="a" title={linkTitle} aria-label={linkTitle} href={`${getUrlToStackSpotAI()}${url}${item.slug}`} target="_blank">
81
+ <ExternalLink />
82
+ </IconButton>
83
+ </li>
84
+ )
85
+ }
86
+
87
+ const List = <T extends Item>({ selectorConfig, filter, visibility, onSelect }: ListProps<T>) => {
88
+ const t = useTranslate(dictionary)
89
+ const items = selectorConfig.data()
90
+ const filtered = useMemo(() => {
91
+ if (!filter && !visibility) return items
92
+ const lowerFilter = filter?.toLowerCase() ?? ''
93
+
94
+ return items.filter(item =>
95
+ item.slug.toLocaleLowerCase().startsWith(lowerFilter) && (!visibility || item?.visibility_level?.toLowerCase() === visibility),
96
+ )
97
+ }, [filter, items, visibility])
98
+
99
+ if (!items.length) return <Text className="empty" colorScheme="light.700">{t.noData.replace('%', selectorConfig.resourceName)}</Text>
100
+ if (!filtered.length)
101
+ <Text className="empty" colorScheme="light.700">{t.noResults.replace('%', selectorConfig.resourceName)}</Text>
102
+
103
+ return (
104
+ <ul className="selector-list">
105
+ {filtered.map((item) => (
106
+ <ListItem key={item.id} item={item} selectorConfig={selectorConfig} onSelect={onSelect} />
107
+ ))}
108
+ </ul>
109
+ )
110
+ }
111
+
112
+ const SelectorContent = ({ filter, onClose, selectorConfig }: ContentProps<any>) => {
113
+ const t = useTranslate(dictionary)
114
+ const ref = useRef<HTMLDivElement>(null)
115
+ const [visibility, setVisibility] = useState<VisibilityLevelEnum | undefined>()
116
+ const { resourceName, icon, onSelect } = selectorConfig
117
+ const onSelectItem = useCallback((slug: string) => {
118
+ onSelect(slug)
119
+ onClose()
120
+ }, [])
121
+
122
+ useKeyboardControls({
123
+ querySelectors: '.tabs button, button.selector',
124
+ disableTabBehavior: true,
125
+ onPressEscape: onClose,
126
+ onPressArrowLeft: () => (ref.current?.querySelector('.tabs button.active') as HTMLElement)?.focus(),
127
+ onPressArrowRight: () => (ref.current?.querySelector('button.selector') as HTMLElement)?.focus(),
128
+ ref,
129
+ }, [])
130
+
131
+ function createSectionItem(action: VisibilityLevelEnum | undefined) {
132
+ return (
133
+ <li key={action ?? 'all'}>
134
+ <button className={visibility === action ? 'active' : ''} onFocus={() => setVisibility(action)}>
135
+ {t[action || 'all']}
136
+ </button>
137
+ </li>
138
+ )
139
+ }
140
+
141
+ return (
142
+ <div ref={ref}>
143
+ <header>
144
+ <IconBox>{icon}</IconBox>
145
+ <Text as="h3" className="uppercase"> {resourceName} </Text>
146
+ </header>
147
+ <div className="body">
148
+ <ul className="tabs">{sections.map(createSectionItem)}</ul>
149
+ <FallbackBoundary message={t.error.replace('%s', selectorConfig.resourceName)} mini>
150
+ <List filter={filter} visibility={visibility} selectorConfig={selectorConfig} onSelect={onSelectItem} />
151
+ </FallbackBoundary>
152
+ </div>
153
+ </div>
154
+ )
155
+ }
156
+
157
+ export const Selector = <T, >({ inputRef, selectorConfig }: SelectorProps<T>) => {
158
+ const { shortcut, regex, isEnabled } = selectorConfig
159
+ const [isClosed, setClosed] = useState(false)
160
+ const selectorRef = useRef<HTMLDivElement>(null)
161
+ const value = useCurrentChatState('nextMessage') ?? ''
162
+
163
+ const filter = useMemo(() => value === shortcut || regex.test(value) ? value.substring(1) : undefined, [value])
164
+ const shouldRender = isEnabled && filter !== undefined && !isClosed
165
+
166
+ // Resets the closed state whenever the message input is cleared
167
+ useEffect(() => {
168
+ if (!value) setClosed(false)
169
+ }, [value])
170
+
171
+ // Creates the following behavior while the user types in the message input:
172
+ // auto-complete on tab; move focus to the resource panel on press up or down; and close the resource panel on esc.
173
+ useEffect(() => {
174
+ function getFirst() {
175
+ return selectorRef.current?.querySelector('.selector') as HTMLElement | null
176
+ }
177
+
178
+ function onKeyDown(event: Event) {
179
+ const key = (event as KeyboardEvent).key
180
+ if (!selectorRef.current) return
181
+ if (key === 'Tab') {
182
+ getFirst()?.click()
183
+ event.preventDefault()
184
+ } else if (key === 'ArrowDown' || key === 'ArrowUp') {
185
+ getFirst()?.focus()
186
+ event.preventDefault()
187
+ }
188
+ if (key === 'Escape') {
189
+ setClosed(true)
190
+ }
191
+ }
192
+
193
+ inputRef.current?.addEventListener('keydown', onKeyDown)
194
+ return () => inputRef.current?.removeEventListener('keydown', onKeyDown)
195
+ }, [])
196
+
197
+ // Closes the panel when the user clicks outside the panel or the message input.
198
+ useEffect(() => {
199
+ if (!shouldRender) return
200
+ function onClickOut(e: Event) {
201
+ const target = e.target as HTMLElement | null
202
+ if (!selectorRef.current?.contains(target) && !inputRef.current?.contains(target)) setClosed(true)
203
+ }
204
+ document.addEventListener('click', onClickOut)
205
+ return () => document.removeEventListener('click', onClickOut)
206
+ }, [shouldRender])
207
+
208
+ return (
209
+ <SelectorBox>
210
+ <Fading visible={shouldRender} ref={selectorRef} className="box-selector">
211
+ <SelectorContent
212
+ selectorConfig={selectorConfig}
213
+ filter={filter}
214
+ onClose={() => setClosed(true)}
215
+ inputRef={inputRef}
216
+ />
217
+ </Fading>
218
+ </SelectorBox>
219
+ )
220
+ }
221
+
222
+ const dictionary = {
223
+ en: {
224
+ all: 'All',
225
+ personal: 'Personal',
226
+ account: 'Account',
227
+ shared: 'Shared',
228
+ workspace: 'Workspace',
229
+ error: 'Could not load the %ss.',
230
+ noData: 'You don\'t have any %s yet.',
231
+ noResults: 'There are no %ss to show here.',
232
+ open: 'Open this %s settings in a new tab.',
233
+ },
234
+ pt: {
235
+ all: 'Todos',
236
+ personal: 'Pessoal',
237
+ account: 'Conta',
238
+ shared: 'Compartilhado',
239
+ workspace: 'Workspace',
240
+ error: 'Não foi possível carregar os %ss.',
241
+ noData: 'Você ainda não possui %ss.',
242
+ noResults: 'Não há %ss para mostrar aqui.',
243
+ open: 'Abra as configurações deste %s em uma nova aba.',
244
+ },
245
+ } satisfies Dictionary
@@ -0,0 +1,145 @@
1
+ import { theme } from '@stack-spot/portal-theme'
2
+ import { styled } from 'styled-components'
3
+
4
+
5
+ export const SelectorBox = styled.div`
6
+ .box-selector {
7
+ position: absolute;
8
+ border-radius: 4px;
9
+ border: 1px solid ${theme.color.light[600]};
10
+ background-color: ${theme.color.light[400]};
11
+ box-shadow: 0px 2px 16px 0px #0000005C;
12
+ display: flex;
13
+ flex-direction: column;
14
+ width: 480px;
15
+ bottom: 74px;
16
+
17
+ .loading, .error {
18
+ padding-bottom: 26px;
19
+ p {
20
+ width: 200px;
21
+ text-align: center;
22
+ line-height: 20px;
23
+ }
24
+ }
25
+
26
+ .empty {
27
+ padding-bottom: 26px;
28
+ width: 200px;
29
+ text-align: center;
30
+ line-height: 20px;
31
+ margin: auto;
32
+ }
33
+
34
+ header {
35
+ display: flex;
36
+ flex-direction: row;
37
+ gap: 8px;
38
+ align-items: center;
39
+ padding: 8px;
40
+ margin-bottom: 4px;
41
+ font-family: 'San Francisco';
42
+ text-transform: uppercase;
43
+ font-weight: 500;
44
+ font-size: 11px;
45
+ background-color: ${theme.color.light[500]};
46
+ }
47
+
48
+ .body {
49
+ display: flex;
50
+ flex-direction: row;
51
+ align-items: center;
52
+ }
53
+
54
+ ul {
55
+ margin: 0;
56
+ padding: 0;
57
+ list-style: none;
58
+ }
59
+
60
+ ul.tabs {
61
+ display: flex;
62
+ flex-direction: column;
63
+
64
+ li {
65
+ display: flex;
66
+ flex-direction: column;
67
+ }
68
+
69
+ button {
70
+ box-sizing: border-box;
71
+ color: ${theme.color.light[700]};
72
+ text-align: left;
73
+ padding: 10px;
74
+ font-weight: 600;
75
+ font-size: 12px;
76
+ transition: background-color 0.3s;
77
+ border-left: 1px solid transparent;
78
+ border-top-right-radius: 4px;
79
+ border-bottom-right-radius: 4px;
80
+ background-color: transparent;
81
+ border: none;
82
+ cursor: pointer;
83
+ outline: none;
84
+
85
+ &:hover, &.active, &:focus {
86
+ background-color: ${theme.color.light[600]};
87
+ }
88
+
89
+ &.active {
90
+ border-left: 1px solid ${theme.color.light.contrastText};
91
+ color: ${theme.color.light.contrastText};
92
+ }
93
+ }
94
+ }
95
+
96
+ ul.selector-list {
97
+ align-self: stretch;
98
+ display: flex;
99
+ flex-direction: column;
100
+ gap: 2px;
101
+ overflow-y: auto;
102
+ flex: 1;
103
+ max-height: 170px;
104
+
105
+ li {
106
+ display: flex;
107
+ flex-direction: row;
108
+ align-items: center;
109
+ gap: 8px;
110
+ padding: 8px;
111
+ border-radius: 4px;
112
+
113
+ &:hover, &.focus {
114
+ background-color: ${theme.color.light[600]};
115
+ }
116
+
117
+ button.selector {
118
+ flex: 1;
119
+ border: none;
120
+ text-align: left;
121
+ background-color: transparent;
122
+ text-align: left;
123
+ outline: none;
124
+ overflow: hidden;
125
+ cursor: pointer;
126
+
127
+ .selector-title {
128
+ font-size: 11px;
129
+ margin: 0 0 4px 0;
130
+ color: ${theme.color.light.contrastText};
131
+ text-transform: uppercase;
132
+ text-overflow: ellipsis;
133
+ overflow: hidden;
134
+ }
135
+
136
+ .selector-description {
137
+ color: ${theme.color.light[700]};
138
+ font-size: 12px;
139
+ margin: 0;
140
+ }
141
+ }
142
+ }
143
+ }
144
+ }
145
+ `
package/src/regex.ts CHANGED
@@ -1 +1,2 @@
1
1
  export const quickCommandRegex = /^\/[\w\d-_]+$/
2
+ export const agentRegex = /^@[\w\d-_]+$/
@@ -1,5 +1,5 @@
1
- import { Button, Text } from '@citric/core'
2
- import { Search } from '@citric/icons'
1
+ import { Button, IconBox, Text } from '@citric/core'
2
+ import { Agent, Search } from '@citric/icons'
3
3
  import { Placeholder } from '@stack-spot/portal-components/Placeholder'
4
4
  import { MiniLogo } from '@stack-spot/portal-components/svg'
5
5
  import { agentClient } from '@stack-spot/portal-network'
@@ -20,7 +20,7 @@ export const AgentsTab = ({ visibility }: { visibility: VisibilityLevel | 'BUILT
20
20
  const [filter, setFilter] = useState('')
21
21
  const defaultAgent = useMemo(() => ({
22
22
  id: '',
23
- name: 'Stackspot AI',
23
+ name: 'StackSpot AI',
24
24
  description: t.defaultAgentDescription,
25
25
  llm_config: { model_slug: 'gpt4o' },
26
26
  } as AgentResponse), [])
@@ -54,7 +54,7 @@ export const AgentsTab = ({ visibility }: { visibility: VisibilityLevel | 'BUILT
54
54
  onChange={setValue}
55
55
  renderLabel={({ name, avatar, id }) => (
56
56
  <AgentLabel>
57
- {id ? (avatar && <img src={avatar} />) : <MiniLogo />}
57
+ {id ? (avatar ? <img src={avatar} /> : <IconBox size="xs"><Agent /></IconBox>) : <MiniLogo />}
58
58
  <Text>{name}</Text>
59
59
  </AgentLabel>
60
60
  )}
@@ -15,6 +15,6 @@ export const AgentInfo = ({ agent }: Props) => (
15
15
  ? <img src={agent.image} className="custom-agent-image" />
16
16
  : <div className="default-image-wrapper"><MiniLogo className="agent-image" /></div>
17
17
  }
18
- <Text appearance="body2">{agent?.label || 'Stackspot AI'}</Text>
18
+ <Text appearance="body2">{agent?.label || 'StackSpot AI'}</Text>
19
19
  </>
20
20
  )
@@ -1,5 +1,5 @@
1
- import { Text } from '@citric/core'
2
- import { MiniLogo } from '@stack-spot/portal-components/svg'
1
+ import { IconBox, Text } from '@citric/core'
2
+ import { Agent } from '@citric/icons'
3
3
  import { agentClient } from '@stack-spot/portal-network'
4
4
  import { theme } from '@stack-spot/portal-theme'
5
5
  import { useMemo } from 'react'
@@ -31,7 +31,7 @@ export const CustomAgent = () => {
31
31
 
32
32
  return (
33
33
  <HomeBox className="home-page custom-agent">
34
- {image ? <img src={image} className="avatar" /> : <MiniLogo className="avatar" />}
34
+ {image ? <img src={image} className="avatar" /> : <IconBox className="avatar"><Agent /></IconBox>}
35
35
  <Text appearance="h3">{label}</Text>
36
36
  <div className="shortcuts">{suggestions?.length ? suggestions : null}</div>
37
37
  </HomeBox>
@@ -32,7 +32,7 @@ export const HomeBox = styled.div`
32
32
  }
33
33
  }
34
34
 
35
- .avatar {
35
+ .avatar, .avatar svg {
36
36
  width: 74px;
37
37
  height: 74px;
38
38
  border-radius: 50%;
@@ -0,0 +1,35 @@
1
+ import { Agent } from '@citric/icons'
2
+ import { agentClient } from '@stack-spot/portal-network'
3
+ import { AgentResponse } from '@stack-spot/portal-network/api/agent'
4
+ import { useCallback } from 'react'
5
+ import { Selector } from '../../components/Selector'
6
+ import { useCurrentChat, useCurrentChatState } from '../../context/hooks'
7
+ import { agentRegex } from '../../regex'
8
+
9
+ export const AgentSelector = ({ inputRef }: { inputRef: React.RefObject<HTMLTextAreaElement | HTMLInputElement> }) => {
10
+ const chat = useCurrentChat()
11
+ const onSelectItem = useCallback((agent: AgentResponse) => {
12
+ const newValue = `@${agent.slug}`
13
+ chat.set('nextMessage', undefined)
14
+ chat.set('agent', { id: agent.id, label: agent.slug, image: agent.avatar, builtIn: false })
15
+
16
+ if (!inputRef.current) return
17
+ inputRef.current.value = newValue
18
+ inputRef.current.focus()
19
+ }, [])
20
+
21
+ return <Selector
22
+ inputRef={inputRef}
23
+ selectorConfig={{
24
+ resourceName: 'Agent',
25
+ shortcut: '@',
26
+ icon: <Agent />,
27
+ imageProp: 'avatar',
28
+ regex: agentRegex,
29
+ url: '/agent/',
30
+ data: () => agentClient.agents.useQuery({}),
31
+ isEnabled: useCurrentChatState('features').agent,
32
+ onSelect: onSelectItem,
33
+ }}
34
+ />
35
+ }
@@ -0,0 +1,36 @@
1
+ import { Flex, IconBox } from '@citric/core'
2
+ import { Agent, TimesMini } from '@citric/icons'
3
+ import { IconButton, Tooltip } from '@citric/ui'
4
+ import { MiniLogo } from '@stack-spot/portal-components/svg'
5
+ import { useCurrentChat, useCurrentChatState, useWidget } from '../../context/hooks'
6
+ import { useMessageInputDictionary } from './dictionary'
7
+
8
+ export const ButtonAgent = () => {
9
+ const t = useMessageInputDictionary()
10
+ const widget = useWidget()
11
+ const chat = useCurrentChat()
12
+ const agent = useCurrentChatState('agent')
13
+ const features = useCurrentChatState('features')
14
+
15
+ return (
16
+ <div className="button-group">
17
+ {features.agent && (
18
+ <div className="group-agent">
19
+ <IconButton aria-label={t.agent} title={t.agent} className="agent" onClick={() => widget.set('panel', 'agent')}>
20
+ <MiniLogo />
21
+ </IconButton>
22
+ {agent?.id &&
23
+ <Tooltip text={t.remove} >
24
+ <IconButton aria-label={t.remove} className="agent agent-selected" onClick={() => chat.set('agent', undefined)}>
25
+ {agent?.image ? <img src={agent.image} className="image" /> : <IconBox className="image" size="xs"><Agent /></IconBox>}
26
+ <Flex className="icon-remove" alignContent="center" justifyContent="center">
27
+ <TimesMini />
28
+ </Flex>
29
+ </IconButton>
30
+ </Tooltip>
31
+ }
32
+ </div>
33
+ )}
34
+ </div>
35
+ )
36
+ }
@@ -1,6 +1,5 @@
1
1
  import { ChevronRight, Code, KnowledgeSource, Send, Stack, Times, Workspace } from '@citric/icons'
2
2
  import { IconButton } from '@citric/ui'
3
- import { MiniLogo } from '@stack-spot/portal-components/svg'
4
3
  import { listToClass } from '@stack-spot/portal-theme'
5
4
  import { useEffect, useRef } from 'react'
6
5
  import { useCurrentChatState, useWidget } from '../../context/hooks'
@@ -31,16 +30,15 @@ interface ButtonGroupProps {
31
30
 
32
31
  /**
33
32
  * Renders the button group at right bottom side of the message input. This includes the send button as well as the buttons to open the
34
- * editor, change the agent, the stack, etc.
33
+ * editor, change the stack, etc.
35
34
  */
36
35
  export const ButtonGroup = ({ onSend, onCancel, expanded, setExpanded, isLoading }: ButtonGroupProps) => {
37
36
  const t = useMessageInputDictionary()
38
37
  const widget = useWidget()
39
38
  const featureButtonsWidth = useRef<number | undefined>()
40
39
  const featureButtons = useRef<HTMLDivElement>(null)
41
- const agent = useCurrentChatState('agent')
42
40
  const features = useCurrentChatState('features')
43
- const hasFeatureButtons = features.agent || features.workspace || features.knowledgeSource || features.stack || features.editor
41
+ const hasFeatureButtons = features.workspace || features.knowledgeSource || features.stack || features.editor
44
42
 
45
43
  useEffect(
46
44
  () => {
@@ -52,7 +50,7 @@ export const ButtonGroup = ({ onSend, onCancel, expanded, setExpanded, isLoading
52
50
  else featureButtons.current.style.width = `${featureButtonsWidth.current}px`
53
51
  },
54
52
  // don't use the whole features object here, it would make every chat tab change rerun this effect.
55
- [features.agent, features.workspace, features.knowledgeSource, features.stack, features.editor],
53
+ [features.workspace, features.knowledgeSource, features.stack, features.editor],
56
54
  )
57
55
 
58
56
  return (
@@ -63,11 +61,6 @@ export const ButtonGroup = ({ onSend, onCancel, expanded, setExpanded, isLoading
63
61
  className={listToClass(['feature-buttons', expanded && 'expanded'])}
64
62
  style={{ width: expanded ? featureButtonsWidth.current : 0 }}
65
63
  >
66
- {features.agent && (
67
- <IconButton aria-label={t.agent} title={t.agent} className="agent" onClick={() => widget.set('panel', 'agent')}>
68
- {agent?.image ? <img src={agent.image} /> : <MiniLogo />}
69
- </IconButton>
70
- )}
71
64
  {features.workspace && (
72
65
  <IconButton aria-label={t.workspace} title={t.workspace} onClick={() => widget.set('panel', 'workspace')}>
73
66
  <Workspace />