@stack-spot/ai-chat-widget 1.6.0 → 1.7.0-beta.1

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,218 +1,34 @@
1
- import { IconBox, Text } from '@citric/core'
2
- import { ExternalLink, QuickCommand } from '@citric/icons'
3
- import { IconButton } from '@citric/ui'
4
- import { useKeyboardControls } from '@stack-spot/portal-components'
1
+ import { QuickCommand } from '@citric/icons'
5
2
  import { aiClient } from '@stack-spot/portal-network'
6
- import { QuickCommandListResponse, VisibilityLevelEnum } from '@stack-spot/portal-network/api/ai'
7
- import { Dictionary, useTranslate } from '@stack-spot/portal-translate'
8
- import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
9
- import { Fading } from '../../components/Fading'
10
- import { FallbackBoundary } from '../../components/FallbackBoundary'
3
+ import { QuickCommandListResponse } from '@stack-spot/portal-network/api/ai'
4
+ import { useCallback } from 'react'
5
+ import { Selector } from '../../components/Selector'
11
6
  import { useCurrentChat, useCurrentChatState } from '../../context/hooks'
12
7
  import { quickCommandRegex } from '../../regex'
13
- import { getUrlToStackSpotAI } from '../../utils/url'
14
8
 
15
- interface Props {
16
- /**
17
- * A reference to the input this quick commands panel is attached to.
18
- */
19
- inputRef: React.RefObject<HTMLTextAreaElement | HTMLInputElement>,
20
- }
21
-
22
- interface ContentProps extends Props {
23
- filter?: string,
24
- onClose: () => void,
25
- }
26
-
27
- interface ListProps {
28
- filter?: string,
29
- visibility?: VisibilityLevelEnum,
30
- onSelect: (slug: string) => void,
31
- }
32
-
33
- interface ItemProps {
34
- qc: QuickCommandListResponse,
35
- onSelect: (slug: string) => void,
36
- }
37
-
38
- const sections = [undefined, 'personal', 'workspace', 'account', 'shared'] as const
39
-
40
- const CommandListItem = ({ qc, onSelect }: ItemProps) => {
41
- const t = useTranslate(dictionary)
42
- return (
43
- <li>
44
- <button
45
- className="qc"
46
- onClick={() => onSelect(qc.slug)}
47
- // the following line prevents a new line character in the message when the user presses enter to select a qc.
48
- onKeyDown={e => e.key === 'Enter' && e.preventDefault()}
49
- onFocus={e => e.target.closest('li')?.classList.add('focus')}
50
- onBlur={e => e.target.closest('li')?.classList.remove('focus')}
51
- >
52
- <p className="qc-title">/{qc.slug}</p>
53
- <p className="qc-description">{qc.description}</p>
54
- </button>
55
- <IconButton as="a" title={t.openQC} aria-label={t.openQC} href={`${getUrlToStackSpotAI()}/quick-command/${qc.slug}`} target="_blank">
56
- <ExternalLink />
57
- </IconButton>
58
- </li>
59
- )
60
- }
61
-
62
- const CommandList = ({ filter, visibility, onSelect }: ListProps) => {
63
- const t = useTranslate(dictionary)
64
- const quickCommands = aiClient.quickCommands.useQuery({ order: 'a-to-z' })
65
- let filtered = quickCommands
66
-
67
- if (visibility || filter) {
68
- const lowerFilter = filter?.toLocaleLowerCase()
69
- filtered = quickCommands.filter(
70
- qc => (!lowerFilter || qc.slug.toLocaleLowerCase().startsWith(lowerFilter)) && (!visibility || qc.visibility_level === visibility),
71
- )
72
- }
73
- if (!quickCommands.length) return <Text className="empty" colorScheme="light.700">{t.noData}</Text>
74
- if (!filtered.length) return <Text className="empty" colorScheme="light.700">{t.noResults}</Text>
75
- return (
76
- <ul className="command-list">
77
- {filtered.map(qc => <CommandListItem key={qc.id} qc={qc} onSelect={onSelect} />)}
78
- </ul>
79
- )
80
- }
81
-
82
- const SelectorContent = ({ filter, onClose, inputRef }: ContentProps) => {
83
- const t = useTranslate(dictionary)
84
- const ref = useRef<HTMLDivElement>(null)
9
+ export const QuickCommandSelector = ({ inputRef }: { inputRef: React.RefObject<HTMLTextAreaElement | HTMLInputElement> }) => {
85
10
  const chat = useCurrentChat()
86
- const [visibility, setVisibility] = useState<VisibilityLevelEnum | undefined>()
87
11
 
88
- const onSelectQC = useCallback((slug: string) => {
89
- const newValue = `/${slug}`
12
+ const onSelectItem = useCallback((qc: QuickCommandListResponse) => {
13
+ const newValue = `/${qc.slug}`
90
14
  chat.set('nextMessage', newValue)
91
- onClose()
15
+
92
16
  if (!inputRef.current) return
93
- // the following line prevents bugs by setting the text area value before react gets the chance to.
94
17
  inputRef.current.value = newValue
95
18
  inputRef.current.focus()
96
19
  }, [])
97
20
 
98
- useKeyboardControls({
99
- querySelectors: '.tabs button, button.qc',
100
- disableTabBehavior: true,
101
- onPressEscape: onClose,
102
- onPressArrowLeft: () => (ref.current?.querySelector('.tabs button.active') as HTMLElement)?.focus(),
103
- onPressArrowRight: () => (ref.current?.querySelector('button.qc') as HTMLElement)?.focus(),
104
- ref,
105
- }, [])
106
-
107
- function createSectionItem(action: VisibilityLevelEnum | undefined) {
108
- return (
109
- <li key={action ?? 'all'}>
110
- <button className={visibility === action ? 'active' : ''} onFocus={() => setVisibility(action)}>
111
- {t[action || 'all']}
112
- </button>
113
- </li>
114
- )
115
- }
116
-
117
- return (
118
- <div ref={ref}>
119
- <header>
120
- <IconBox><QuickCommand /></IconBox>
121
- <Text as="h3">QUICK COMMANDS</Text>
122
- </header>
123
- <div className="body">
124
- <ul className="tabs">{sections.map(createSectionItem)}</ul>
125
- <FallbackBoundary message={t.error} mini>
126
- <CommandList onSelect={onSelectQC} filter={filter} visibility={visibility} />
127
- </FallbackBoundary>
128
- </div>
129
- </div>
130
- )
21
+ return <Selector
22
+ inputRef={inputRef}
23
+ selectorConfig={{
24
+ resourceName: 'Quick Command',
25
+ shortcut: '/',
26
+ icon: <QuickCommand />,
27
+ url: '/quick-command/',
28
+ regex: quickCommandRegex,
29
+ data: () => aiClient.quickCommands.useQuery({ order: 'a-to-z' }),
30
+ isEnabled: useCurrentChatState('features').quickCommands,
31
+ onSelect: onSelectItem,
32
+ }}
33
+ />
131
34
  }
132
-
133
- /**
134
- * This renders the floating Quick Commands panel that allows the user to select a quick command. This appears whenever the user types "/"
135
- * in the textarea.
136
- */
137
- export const QuickCommandSelector = ({ inputRef }: Props) => {
138
- const value = useCurrentChatState('nextMessage') ?? ''
139
- const filter = useMemo(() => value === '/' || quickCommandRegex.test(value) ? value.substring(1) : undefined, [value])
140
- const [isClosed, setClosed] = useState(false)
141
- const selectorRef = useRef<HTMLDivElement>(null)
142
- const isEnabled = useCurrentChatState('features').quickCommands
143
- const shouldRender = isEnabled && filter !== undefined && !isClosed
144
-
145
- // Resets the closed state whenever the message input is cleared
146
- useEffect(() => {
147
- if (!value) setClosed(false)
148
- }, [value])
149
-
150
- // Creates the following behavior while the user types in the message input:
151
- // auto-complete on tab; move focus to the qc panel on press up or down; and close the qc panel on esc.
152
- useEffect(() => {
153
- function getFirst() {
154
- return selectorRef.current?.querySelector('.qc') as HTMLElement | null
155
- }
156
-
157
- function onKeyDown(event: Event) {
158
- const key = (event as KeyboardEvent).key
159
- if (!selectorRef.current) return
160
- if (key === 'Tab') {
161
- getFirst()?.click()
162
- event.preventDefault()
163
- }
164
- else if (key === 'ArrowDown' || key === 'ArrowUp') {
165
- getFirst()?.focus()
166
- event.preventDefault()
167
- }
168
- if (key === 'Escape') {
169
- setClosed(true)
170
- }
171
- }
172
-
173
- inputRef.current?.addEventListener('keydown', onKeyDown)
174
- return () => inputRef.current?.removeEventListener('keydown', onKeyDown)
175
- }, [])
176
-
177
- // Closes the panel when the user clicks outside the qc panel or the message input.
178
- useEffect(() => {
179
- if (!shouldRender) return
180
- function onClickOut(e: Event) {
181
- const target = e.target as HTMLElement | null
182
- if (!selectorRef.current?.contains(target) && !inputRef.current?.contains(target)) setClosed(true)
183
- }
184
- document.addEventListener('click', onClickOut)
185
- return () => document.removeEventListener('click', onClickOut)
186
- }, [shouldRender])
187
-
188
- return (
189
- <Fading visible={shouldRender} ref={selectorRef} className="quick-command-selector">
190
- <SelectorContent filter={filter} onClose={() => setClosed(true)} inputRef={inputRef} />
191
- </Fading>
192
- )
193
- }
194
-
195
- const dictionary = {
196
- en: {
197
- all: 'All',
198
- personal: 'Personal',
199
- account: 'Account',
200
- shared: 'Shared',
201
- workspace: 'Workspace',
202
- error: 'Could not load the quick commands.',
203
- noData: 'You don\'t have any quick command yet.',
204
- noResults: 'There are no quick commands to show here.',
205
- openQC: 'Open this quick command\'s settings in a new tab.',
206
- },
207
- pt: {
208
- all: 'Todos',
209
- personal: 'Pessoal',
210
- account: 'Conta',
211
- shared: 'Compartilhado',
212
- workspace: 'Workspace',
213
- error: 'Não foi possível carregar os quick commands.',
214
- noData: 'Você ainda não possui quick commands.',
215
- noResults: 'Não há quick commands para mostrar aqui.',
216
- openQC: 'Abra as configurações deste quick command em uma nova aba.',
217
- },
218
- } satisfies Dictionary
@@ -10,7 +10,7 @@ const dictionary = {
10
10
  collapse: 'Hide buttons',
11
11
  expand: 'Show buttons',
12
12
  send: 'Send message',
13
- placeholder: 'Type your prompt',
13
+ placeholder: 'Message to %s',
14
14
  cancel: 'Cancel',
15
15
  removeConfig: 'Remove all the configuration',
16
16
  removeStack: 'Stop using the current stack',
@@ -18,6 +18,7 @@ const dictionary = {
18
18
  removeKS: 'Stop using this knowledge source',
19
19
  selected: 'Selected',
20
20
  removeSelection: 'Remove current code selection',
21
+ remove: 'Remove',
21
22
  },
22
23
  pt: {
23
24
  stack: 'Selecionar stack',
@@ -28,7 +29,7 @@ const dictionary = {
28
29
  collapse: 'Esconder botões',
29
30
  expand: 'Mostrar botões',
30
31
  send: 'Enviar mensagem',
31
- placeholder: 'Digite sua pergunta',
32
+ placeholder: 'Mensagem para %s',
32
33
  cancel: 'Cancelar',
33
34
  removeConfig: 'Remover todas as configurações',
34
35
  removeStack: 'Parar de usar a stack atual',
@@ -36,6 +37,7 @@ const dictionary = {
36
37
  removeKS: 'Parar de usar este knowledge source',
37
38
  selected: 'Selecionado',
38
39
  removeSelection: 'Desfazer seleção de código',
40
+ remove: 'Remover',
39
41
  },
40
42
  } satisfies Dictionary
41
43
 
@@ -5,6 +5,8 @@ import { ProgressBar } from '../../components/ProgressBar'
5
5
  import { useCurrentChat, useCurrentChatState, useWidgetState } from '../../context/hooks'
6
6
  import { quickCommandRegex } from '../../regex'
7
7
  import { ChatEntry } from '../../state/ChatEntry'
8
+ import { AgentSelector } from './AgentSelector'
9
+ import { ButtonAgent } from './ButtonAgent'
8
10
  import { ButtonGroup } from './ButtonGroup'
9
11
  import { useMessageInputDictionary } from './dictionary'
10
12
  import { InfoBar } from './InfoBar'
@@ -26,7 +28,8 @@ export const MessageInput = () => {
26
28
  const value = useCurrentChatState('nextMessage') ?? ''
27
29
  const isMinimized = useWidgetState('isMinimized')
28
30
  const textAreaRef = useRef<HTMLTextAreaElement>(null)
29
-
31
+ const agentLabel = useCurrentChatState('agent')?.label ?? 'Stackspot AI'
32
+
30
33
  const onSend = useCallback(async () => {
31
34
  const message = chat.get('nextMessage')
32
35
  if (!message) return
@@ -53,12 +56,14 @@ export const MessageInput = () => {
53
56
  <MessageInputBox aria-busy={isLoading} className="message-input">
54
57
  <ProgressBar visible={isLoading} shimmer />
55
58
  <InfoBar />
59
+ <QuickCommandSelector inputRef={textAreaRef} />
60
+ <AgentSelector inputRef={textAreaRef} />
56
61
  <div className={listToClass(['action-box', focused && 'focused', isLoading && 'disabled'])}>
57
- <QuickCommandSelector inputRef={textAreaRef} />
62
+ <ButtonAgent />
58
63
  <AdaptiveTextArea
59
64
  ref={textAreaRef}
60
65
  disabled={isLoading}
61
- placeholder={t.placeholder}
66
+ placeholder={t.placeholder.replace('%s', agentLabel)}
62
67
  onChange={e => chat.set('nextMessage', e.target.value)}
63
68
  value={value}
64
69
  onFocus={() => setFocused(true)}
@@ -88,12 +88,12 @@ export const MessageInputBox = styled.div`
88
88
  display: flex;
89
89
  position: relative;
90
90
  flex-direction: row;
91
- gap: 4px;
91
+ gap: 8px;
92
92
  align-items: end;
93
93
  border-radius: 4px;
94
94
  border: 1px solid ${theme.color.light[500]};
95
95
  background-color: ${theme.color.light[300]};
96
- padding: 8px 10px;
96
+ padding: 10px 8px;
97
97
  transition: border-color 0.3s, background-color 0.3s;
98
98
 
99
99
  &.focused {
@@ -150,12 +150,16 @@ export const MessageInputBox = styled.div`
150
150
  svg {
151
151
  transform: rotate(180deg);
152
152
  }
153
- }
154
-
155
- &.agent img {
156
- width: 80%;
157
- height: 80%;
158
- border-radius: 50%;
153
+ }
154
+
155
+ &.agent {
156
+ border-radius: 50%;
157
+ opacity: 1;
158
+ img {
159
+ width: 100%;
160
+ height: 100%;
161
+ border-radius: 50%;
162
+ }
159
163
  }
160
164
  }
161
165
 
@@ -191,6 +195,45 @@ export const MessageInputBox = styled.div`
191
195
  width: 24px;
192
196
  height: 24px;
193
197
  }
198
+
199
+ .group-agent {
200
+ display: flex;
201
+ margin-left: 0.5rem;
202
+ margin-right: 0.5rem;
203
+
204
+ button {
205
+ margin-right: -0.5rem;
206
+ margin-left: -0.5rem;
207
+ border-radius: 50%;
208
+ background-color: ${theme.color.light[300]};
209
+ border: 1px solid ${theme.color.light[600]};
210
+ display: flex;
211
+
212
+ &.agent-selected:hover:before {
213
+ content: '';
214
+ width: 22px;
215
+ height: 22px;
216
+ position: absolute;
217
+ background-color: ${theme.color.light[300]};
218
+ border-radius: 100%;
219
+ opacity: 0.4;
220
+ }
221
+
222
+ .icon-remove {
223
+ display: none;
224
+ fill: ${theme.color.light.contrastText};
225
+ position: absolute;
226
+ }
227
+
228
+ &:hover .icon-remove {
229
+ display: flex;
230
+ }
231
+
232
+ .image {
233
+ margin: 1px;
234
+ }
235
+ }
236
+ }
194
237
  }
195
238
 
196
239
  textarea {
@@ -206,140 +249,4 @@ export const MessageInputBox = styled.div`
206
249
  box-shadow: none;
207
250
  }
208
251
  }
209
-
210
- .quick-command-selector {
211
- position: absolute;
212
- border-radius: 4px;
213
- border: 1px solid ${theme.color.light[600]};
214
- background-color: ${theme.color.light[500]};
215
- box-shadow: 0px 2px 16px 0px #0000005C;
216
- display: flex;
217
- flex-direction: column;
218
- width: 480px;
219
- bottom: 55px;
220
-
221
- .loading, .error {
222
- padding-bottom: 26px;
223
- p {
224
- width: 200px;
225
- text-align: center;
226
- line-height: 20px;
227
- }
228
- }
229
-
230
- .empty {
231
- padding-bottom: 26px;
232
- width: 200px;
233
- text-align: center;
234
- line-height: 20px;
235
- margin: auto;
236
- }
237
-
238
- header {
239
- display: flex;
240
- flex-direction: row;
241
- gap: 8px;
242
- align-items: center;
243
- padding: 8px;
244
- font-family: 'San Francisco';
245
- font-weight: 500;
246
- font-size: 11px;
247
- }
248
-
249
- .body {
250
- display: flex;
251
- flex-direction: row;
252
- align-items: center;
253
- }
254
-
255
- ul {
256
- margin: 0;
257
- padding: 0;
258
- list-style: none;
259
- }
260
-
261
- ul.tabs {
262
- display: flex;
263
- flex-direction: column;
264
-
265
- li {
266
- display: flex;
267
- flex-direction: column;
268
- }
269
-
270
- button {
271
- box-sizing: border-box;
272
- color: ${theme.color.light[700]};
273
- text-align: left;
274
- padding: 10px;
275
- font-weight: 600;
276
- font-size: 12px;
277
- transition: background-color 0.3s;
278
- border-top-right-radius: 4px;
279
- border-bottom-right-radius: 4px;
280
- background-color: transparent;
281
- border: none;
282
- cursor: pointer;
283
- outline: none;
284
-
285
- &:hover, &.active, &:focus {
286
- background-color: ${theme.color.light[600]};
287
- }
288
-
289
- &.active {
290
- border-left: 1px solid ${theme.color.light.contrastText};
291
- color: ${theme.color.light.contrastText};
292
- }
293
- }
294
- }
295
-
296
- ul.command-list {
297
- align-self: stretch;
298
- display: flex;
299
- flex-direction: column;
300
- gap: 2px;
301
- overflow-y: auto;
302
- flex: 1;
303
- max-height: 170px;
304
-
305
- li {
306
- display: flex;
307
- flex-direction: row;
308
- align-items: center;
309
- gap: 8px;
310
- padding: 8px;
311
- border-radius: 4px;
312
-
313
- &:hover, &.focus {
314
- background-color: ${theme.color.light[600]};
315
- }
316
-
317
- button.qc {
318
- flex: 1;
319
- border: none;
320
- text-align: left;
321
- background-color: transparent;
322
- text-align: left;
323
- outline: none;
324
- overflow: hidden;
325
- cursor: pointer;
326
-
327
- .qc-title {
328
- font-size: 11px;
329
- margin: 0 0 4px 0;
330
- color: ${theme.color.light.contrastText};
331
- text-transform: uppercase;
332
- text-overflow: ellipsis;
333
- overflow: hidden;
334
- }
335
-
336
- .qc-description {
337
- color: ${theme.color.light[700]};
338
- font-size: 12px;
339
- margin: 0;
340
- }
341
- }
342
- }
343
- }
344
- }
345
252
  `