@stack-spot/ai-chat-widget 1.5.2 → 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 (70) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/app-metadata.json +7 -7
  3. package/dist/chat-interceptors/send-message.d.ts.map +1 -1
  4. package/dist/chat-interceptors/send-message.js +16 -8
  5. package/dist/chat-interceptors/send-message.js.map +1 -1
  6. package/dist/components/AdaptiveTextArea.d.ts.map +1 -1
  7. package/dist/components/AdaptiveTextArea.js +7 -1
  8. package/dist/components/AdaptiveTextArea.js.map +1 -1
  9. package/dist/components/Selector/index.d.ts +20 -0
  10. package/dist/components/Selector/index.d.ts.map +1 -0
  11. package/dist/components/Selector/index.js +134 -0
  12. package/dist/components/Selector/index.js.map +1 -0
  13. package/dist/components/Selector/styled.d.ts +2 -0
  14. package/dist/components/Selector/styled.d.ts.map +1 -0
  15. package/dist/components/Selector/styled.js +144 -0
  16. package/dist/components/Selector/styled.js.map +1 -0
  17. package/dist/regex.d.ts +1 -0
  18. package/dist/regex.d.ts.map +1 -1
  19. package/dist/regex.js +1 -0
  20. package/dist/regex.js.map +1 -1
  21. package/dist/views/Agents/AgentsTab.js +4 -4
  22. package/dist/views/Agents/AgentsTab.js.map +1 -1
  23. package/dist/views/Chat/AgentInfo.js +1 -1
  24. package/dist/views/Home/CustomAgent.js +3 -3
  25. package/dist/views/Home/CustomAgent.js.map +1 -1
  26. package/dist/views/Home/styled.js +1 -1
  27. package/dist/views/MessageInput/AgentSelector.d.ts +4 -0
  28. package/dist/views/MessageInput/AgentSelector.d.ts.map +1 -0
  29. package/dist/views/MessageInput/AgentSelector.js +31 -0
  30. package/dist/views/MessageInput/AgentSelector.js.map +1 -0
  31. package/dist/views/MessageInput/ButtonAgent.d.ts +2 -0
  32. package/dist/views/MessageInput/ButtonAgent.d.ts.map +1 -0
  33. package/dist/views/MessageInput/ButtonAgent.js +17 -0
  34. package/dist/views/MessageInput/ButtonAgent.js.map +1 -0
  35. package/dist/views/MessageInput/ButtonGroup.d.ts +1 -1
  36. package/dist/views/MessageInput/ButtonGroup.d.ts.map +1 -1
  37. package/dist/views/MessageInput/ButtonGroup.js +4 -6
  38. package/dist/views/MessageInput/ButtonGroup.js.map +1 -1
  39. package/dist/views/MessageInput/QuickCommandSelector.d.ts +2 -11
  40. package/dist/views/MessageInput/QuickCommandSelector.d.ts.map +1 -1
  41. package/dist/views/MessageInput/QuickCommandSelector.js +17 -130
  42. package/dist/views/MessageInput/QuickCommandSelector.js.map +1 -1
  43. package/dist/views/MessageInput/dictionary.d.ts +1 -1
  44. package/dist/views/MessageInput/dictionary.d.ts.map +1 -1
  45. package/dist/views/MessageInput/dictionary.js +4 -2
  46. package/dist/views/MessageInput/dictionary.js.map +1 -1
  47. package/dist/views/MessageInput/index.d.ts.map +1 -1
  48. package/dist/views/MessageInput/index.js +4 -1
  49. package/dist/views/MessageInput/index.js.map +1 -1
  50. package/dist/views/MessageInput/styled.d.ts.map +1 -1
  51. package/dist/views/MessageInput/styled.js +51 -144
  52. package/dist/views/MessageInput/styled.js.map +1 -1
  53. package/package.json +1 -1
  54. package/src/app-metadata.json +7 -7
  55. package/src/chat-interceptors/send-message.ts +14 -7
  56. package/src/components/AdaptiveTextArea.tsx +8 -1
  57. package/src/components/Selector/index.tsx +245 -0
  58. package/src/components/Selector/styled.ts +145 -0
  59. package/src/regex.ts +1 -0
  60. package/src/views/Agents/AgentsTab.tsx +4 -4
  61. package/src/views/Chat/AgentInfo.tsx +1 -1
  62. package/src/views/Home/CustomAgent.tsx +3 -3
  63. package/src/views/Home/styled.ts +1 -1
  64. package/src/views/MessageInput/AgentSelector.tsx +35 -0
  65. package/src/views/MessageInput/ButtonAgent.tsx +36 -0
  66. package/src/views/MessageInput/ButtonGroup.tsx +3 -10
  67. package/src/views/MessageInput/QuickCommandSelector.tsx +21 -205
  68. package/src/views/MessageInput/dictionary.ts +4 -2
  69. package/src/views/MessageInput/index.tsx +8 -3
  70. package/src/views/MessageInput/styled.ts +51 -144
@@ -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 />
@@ -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)}